I am making a notification feed where you can view the WHOLE comment a user posts on a blog but I am unable to get the whole multi-line comment to show without affecting the spacing/alignment of elements to match the other notifications.
I was able to make the whole comment show by typing in a constant .frame(height: 100) modifier but then EVERY comment notification has that sized of frame.
Is there a way to make the below VStack scale it's frame dynamically based on the comment length or is there a better way based on my code??
Thank you!
struct CommentNotificationCell: View {
#EnvironmentObject var session: SessionStore
var activity: CommentActivity?
var body: some View {
HStack(spacing: 10) {
if let activity = activity {
VStack {
KFImage(URL(string: "\(url)" )!)
Spacer()
}
// Should I scale this VStack's frame height dynamically based
// on the length of the comment..?
VStack(alignment: .leading, spacing: 3) {
HStack(spacing: 3) {
Text(activity.username)
.font(.caption)
Text("commented on your post:")
.font(.caption)
}
Text(activity.comment)
.font(.caption)
Text(activity.createdAt.timeAgoDisplay())
.font(.caption)
}.padding(.leading, 10)
Spacer()
VStack {
ZStack {
Image(systemName: "bubble.left.fill")
.font(.system(size: 13, weight: .regular))
.foregroundColor(.blue)
.zIndex(1)
.offset(x: -25,y: -20)
KFImage(URL(string: "\(url)" )!)
.zIndex(0)
}
Spacer()
}
}
}
}
}
I think your code is already doing that – scaling dynamically based on text length. I cleaned it up a little and for me it works. Is there some other place where you set a specific .frame?
struct ContentView: View {
#State private var test = false
let username = "funnytest"
let shortComment = "kjfdglkj ewoirewoi oikdjsfgdlsfgj 0ewiropsdisg"
let longComment = "kjfdglkj ewoirewoi oikdjsfgdlsfgj 0ewiropsdisg poksaf#ldsifsgali oeirpo dgiodfig odfi ofdgifgpoüi fhfdi mfoidgho miohgfm ogifhogif hiogfh fgihi oogihofgi hofgiho fgopihfgoih pfdgihdfg podfgihofgiho po fdgdfiopugiouü"
var body: some View {
VStack {
CommentNotificationCell(username: username, comment: shortComment)
CommentNotificationCell(username: username, comment: longComment)
}
}
}
struct CommentNotificationCell: View {
var username: String
var comment: String
var body: some View {
HStack(spacing: 10) {
Image(systemName: "person.circle")
.resizable().scaledToFit().frame(width: 60)
VStack(alignment: .leading, spacing: 3) {
HStack(spacing: 3) {
Text(username)
.font(.caption).bold()
Text("commented on your post:")
.font(.caption)
}
Text(comment)
.font(.caption)
Text("8 hours ago")
.font(.caption)
}.padding(.leading, 10)
Spacer()
ZStack {
Image(systemName: "bubble.left.fill")
.font(.system(size: 13, weight: .regular))
.foregroundColor(.blue)
.zIndex(1)
.offset(x: -25,y: -20)
// Image("URL(string: "\(url)" )!")
// .zIndex(0)
}
}
}
}
I found out the fix was to remove the Spacer() in the first VStack
VStack {
KFImage(URL(string: "\(url)" )!)
}
Related
This is my main view where i'm using AsyncAwait to fetch the data. I wanna have the effect of nice animation when i receive the data. Tried a lot of things, but none worked. Some animations were successful but only after opening the specific view for the 3-rd + time. On first opening i could never achieve that effect. Would really appreciate if someone could tell me what should i do. Thanks in advance :)
struct PresentationRootView: View {
// setting up the default value for segmented control
#State private var selectedSegmentedOption: SegmentedSelection = .myPresentations
#ObservedObject var viewModel: PresentationsViewModel
var body: some View {
let columns = [GridItem(.flexible())]
VStack(spacing: 0) {
Picker(selection: $selectedSegmentedOption) {
Text("My Presentations").tag(SegmentedSelection.myPresentations)
Text("Templates").tag(SegmentedSelection.templates)
} label: {}
.pickerStyle(.segmented)
}
ScrollView {
if selectedSegmentedOption == .myPresentations {
if viewModel.shouldShowEmptyScreen {
Text("No data")
}
LazyVGrid(columns: columns, spacing: 4, content: {
ForEach($viewModel.myPresentations, id: \.ID) { element in
PresentationSingleView(url: element.ThumbURLWithSas.wrappedValue, title: element.Name.wrappedValue, numberOfResources: 4)
.padding([.leading, .trailing], 13)
}
}).task {
await viewModel.getAllPresentations(page: 1, pageSize: 10)
}
}
}
.background(CustomColor.background.swiftUIColor)
Spacer()
.navigationTitle("Presentations")
}
}
struct PresentationSingleView: View {
// replace this with the real data
var url: URL
var title: String
var numberOfResources: Int
var isIphone = DefaultAppManager.shared.isIphone
#StateObject var image = FetchImage()
var body: some View {
HStack(alignment: .top) {
image.view?
.resizable()
.scaledToFill()
.frame(maxWidth: isIphone ? 90 : 140, maxHeight: isIphone ? 64 : 140)
.cornerRadius(4)
.padding(isIphone ? 10 : 20)
VStack(alignment: .leading) {
Text(title)
.font(Font.custom("Roboto-Regular", size: 12))
.padding(.top, 20)
Spacer()
HStack(alignment: .center) {
Image("presentationResourceIcon")
Text("\(numberOfResources) resources")
.font(Font.custom("Roboto-Medium", size: 12))
Spacer()
Image("more_options")
.onTapGesture {
print("tapped on right image!!!")
}
}
.padding(.bottom, 20)
}
Spacer()
}
.onAppear {
image.priority = .normal
image.load(url)
}
.frame(height: isIphone ? 90 : 150)
.background(Color.white)
.cornerRadius(8)
}
}
I am trying to make an adaptive CommentViewRow for a social app where the whole comment text can be displayed within the row.
So far I am achieving this but I have 3 issues:
1 When the comment-text is short it is being centred within the row even when I specify ".alignment: .leading" in the VStack.
2 When the comment-text uses more than one line there is a mysterious padding between the user's profile picture & the comment-text?? see image below.
3 I am not sure if my .frame modifier is the best way to achieve what I am doing, it seems mickey-mouse, I was reading about .frame(idealWith, idealHeight, etc..) and not sure if that would help.
Any idea on how I can fix this so that each CommentViewRow displays like your average social-media comment view??
Thank you!
struct CommentViewRow: View {
var comment: Comment
var body: some View {
HStack {
KFImage("profilePicture")
// COMMENT
VStack(alignment: .leading, spacing: 5) {
Text("**\(comment.username)** \(comment.comment)")
.font(.caption)
.frame(width: 310)
.fixedSize(horizontal: true, vertical: false)
Text(comment.createdAt.timeAgoDisplay())
.bold()
.font(.caption)
}
Spacer()
}.padding([.leading, .trailing], 10)
}
}
1st option: If you really need that view to be 310 wide
You can change .frame(width: 310) to .frame(width: 310, alignment: .leading)
2nd option: Let the view adjust itself based on content, you just need to specify the alignment (.leading in this case)
struct CommentViewRow: View {
var comment: Comment
var body: some View {
HStack {
KFImage("profilePicture")
VStack(alignment: .leading, spacing: 5) {
Text("**\(comment.username)** \(comment.comment)")
.font(.caption)
Text(comment.createdAt.timeAgoDisplay())
.font(.caption.bold())
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding([.horizontal], 10)
}
}
yes, get rid of the frame:
struct ContentView: View {
var comment = "Thjfhg jhfgjhdfg jdfhgj dfhdfsjjdfgh djdshfg hjdfgjfdh ghjkf gdhjdfgh jkh fjg dfjkhgj dfglkhdfsg"
var body: some View {
HStack {
Image(systemName: "person.circle")
.font(.largeTitle)
VStack(alignment: .leading, spacing: 5) {
Text("\(comment)")
.font(.caption)
Text("3 minutes ago")
.bold()
.font(.caption)
}
Spacer()
}.padding([.leading, .trailing], 10)
}
}
When i have TextField inside ScrollView and tap on it the keyboard is shown as expected. But it seems that the TextField is moved up just enough to show the input area but i want to be moved enough so that is visible in its whole. Otherwise it looks cropped. I couldn't find a way to change this behaviour.
struct ContentView: View {
#State var text:String = ""
var body: some View {
ScrollView {
VStack(spacing: 10) {
ForEach(1...12, id: \.self) {
Text("\($0)…")
.frame(height:50)
}
TextField("Label..", text: self.$text)
.padding(10)
.background(.white)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.blue, lineWidth: 1)
)
}
.padding()
.background(.red)
}
}
}
Use a .safeAreaInset modifier.
#State var text:String = ""
var body: some View {
ScrollView {
VStack(spacing: 10) {
ForEach(1...12, id: \.self) {
Text("\($0)…")
.frame(height:50)
}
TextField("Label..", text: self.$text)
.padding(10)
.background(.white)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.blue, lineWidth: 1)
)
}
.padding()
.background(.red)
}.safeAreaInset(edge: .bottom) { //this will push the view when the keyboad is shown
Color.clear.frame(height: 30)
}
}
You can provide additional padding to the view (and it works even in iOS 13 and 14):
struct ContentView: View {
#State var text:String = ""
var body: some View {
ScrollView {
VStack(spacing: 10) {
ForEach(1...12, id: \.self) {
Text("\($0)…")
.frame(height:50)
}
TextField("Label..", text: self.$text)
.padding(10)
.background(.white)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.blue, lineWidth: 1)
)
.padding(.bottom, 32) //here, set as much pasding as you want
}
.padding()
.background(.red)
}
}
}
In my iOS App i want to place two views of the same width so that they fill the entire width of the parent view.
For this I use GeometryReader and it broke auto layout. But auto layout does not work and the height of this view is not calculated automatically. Height of TestView is not determined, so i cant add frame size manually...
Here's what it should look like (what i expect TestView):
This is what it looks like when I put a view on a list (CurrenciesView):
TestView.swift
struct TestView: View {
var body: some View {
GeometryReader { geometry in
HStack(spacing: 0) {
VStack(alignment: .leading, spacing: 0.0) {
Text("Name 1\n Test second name 2")
.font(.system(size: 18))
.fontWeight(.bold)
HStack {
Text("123")
Text(" + 5")
}
}
.padding(.horizontal, 12.0)
.padding(.vertical, 9.0)
.frame(width: geometry.size.width / 2)
.background(RoundedRectangle(cornerRadius: 8.0)
.foregroundColor(Color.blue
.opacity(0.2)))
VStack(alignment: .leading, spacing: 0.0) {
Text("Name 1")
.font(.system(size: 18))
.fontWeight(.bold)
HStack {
Text("123")
Text(" + 5")
}
}
.padding(.horizontal, 12.0)
.padding(.vertical, 9.0)
.frame(width: geometry.size.width / 2)
.background(RoundedRectangle(cornerRadius: 8.0)
.foregroundColor(Color.blue
.opacity(0.2)))
}
}
}
}
CurrenciesView.swift
struct CurrenciesView: View {
#State private var items: [Str] = (0..<5).map { i in
return Str(title: "Struct #\(i)")
}
var body: some View {
NavigationView {
List {
Section(header:
TestView().listRowInsets(EdgeInsets())
) {
ForEach(items) { item in
Text("asd")
}
}.clipped()
}
.navigationBarTitle("Section Name")
.navigationBarItems(trailing: EditButton())
}
}
}
You can create a custom PreferenceKey and a view that calculates it:
struct ViewSizeKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
struct ViewGeometry: View {
var body: some View {
GeometryReader { geometry in
Color.clear
.preference(key: ViewSizeKey.self, value: geometry.size)
}
}
}
Then, you can use them in your views. Note that you need to use #Binding in the TestView and #State private var headerSize in the parent view. Otherwise the parent view won't be refreshed and the List won't re-calculate the header size properly.
struct CurrenciesView: View {
#State private var items: [String] = (0 ..< 5).map(String.init)
#State private var headerSize: CGSize = .zero
var body: some View {
NavigationView {
List {
Section(header:
TestView(viewSize: $headerSize)
) {
ForEach(items, id: \.self) {
Text($0)
}
}.clipped()
}
.navigationBarTitle("Section Name")
.navigationBarItems(trailing: EditButton())
}
}
}
struct TestView: View {
#Binding var viewSize: CGSize
var body: some View {
HStack(spacing: 0) {
VStack(alignment: .leading, spacing: 0.0) {
Text("Name 1\n Test second name 2")
.font(.system(size: 18))
.fontWeight(.bold)
HStack {
Text("123")
Text(" + 5")
}
}
.padding(.horizontal, 12.0)
.padding(.vertical, 9.0)
.frame(width: viewSize.width / 2)
.background(RoundedRectangle(cornerRadius: 8.0)
.foregroundColor(Color.blue
.opacity(0.2)))
VStack(alignment: .leading, spacing: 0.0) {
Text("Name 1")
.font(.system(size: 18))
.fontWeight(.bold)
HStack {
Text("123")
Text(" + 5")
}
}
.padding(.horizontal, 12.0)
.padding(.vertical, 9.0)
.frame(width: viewSize.width / 2)
.background(RoundedRectangle(cornerRadius: 8.0)
.foregroundColor(Color.blue
.opacity(0.2)))
}
.frame(maxWidth: .infinity)
.background(ViewGeometry()) // calculate the view size
.onPreferenceChange(ViewSizeKey.self) {
viewSize = $0 // assign the size to `viewSize`
}
}
}
I have a VStack with image and a text, I need to make it clickable so I can show and alert to the user. I have tried onTapGesture method but I'm able to print the statement but alert is not showing up.
Is there any alternate way to get the solution?
I have added the whole swift file for your reference
Code
#State private var hasOneConnected: Bool = false
#State private var showConnectionAlert = false
private var connectionAlert: Alert {
Alert.init(title: Text("Oops!"), message: Text("There is an error."), dismissButton: .default(Text("OK")))
}
var body: some View {
VStack(alignment: .center, spacing: 0) {
// MARK: - Header
VStack(alignment: .center, spacing: 0) {
Spacer().frame(height: 55)
HStack(alignment: .center, spacing: 25) {
Spacer()
Image("Logo")
Spacer(minLength: 5)
Text("Zone Control")
.foregroundColor(.white)
Spacer()
}
Spacer()
}
.frame(height: 100, alignment: .center)
.background(
LinearGradient(
gradient: Gradient(colors: [Color.Heat.primary, Color.Heat.primary.opacity(0.8), Color.Heat.primary.opacity(0.5)]),
startPoint: .top, endPoint: .bottom
)
)
// MARK: - Menu Bar
HStack(alignment: .center, spacing: 10) {
Spacer().frame(maxWidth: 20)
HStack(alignment: .center, spacing: 10) {
Image("batteryModule")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 8)
Text("Pod Status")
.font(.caption)
.foregroundColor(.white)
}
Spacer()
VStack(alignment: .center, spacing: 4) {
Image(self.hasConnected ? "bluetoothConnected" : "bluetoothNotConnected")
Text(self.hasConnected ? "Connected" : "Disconnected")
.font(.caption)
.foregroundColor(.white)
}
.frame(width: 80)
VStack(alignment: .center, spacing: 4) {
Image("batteryStatus")
Text("\(self.batteryLevel)%")
.font(.caption)
.foregroundColor(.white)
}
.frame(width: 60)
Spacer().frame(maxWidth: 10)
}
.padding()
.background(Color.black)
}
.statusBar(hidden: true)
.edgesIgnoringSafeArea(.all)
.onAppear(perform: {
UserDefaults.standard.set(true, forKey: "onboarding")
self.update()
})
.onReceive(self.skiinBLE.objectWillChange) { _ in
self.update()
}
}
func update() {
self.hasOneConnected = self.topLayer.cbPeripheral?.state != .disconnected
self.batteryLevel = self.topLayer.moduleInformation?.batteryLevel.rawValue ?? 0
}
For making empty containers tappable (for example Spacer()), you should use .contentShape(Rectangle()) modifier:
VStack(spacing: 4) {
Image(systemName: "antenna.radiowaves.left.and.right")
Text("Connected")
Spacer()
}
.contentShape(Rectangle())
.onTapGesture {
print("The whole VStack is tappable now!")
}
I have simplified your code to just bare essentials. You don't really need to add a tap gesture, you can wrap the whole element (e.g. a VStack) in a Button and handle triggering the alert from there. Important bit is to remember to set showConnectionAlert back to false when the user taps OK on the Alert. The side effect of wrapping it in a Button is that everything inside will be rendered in the tint colour. That's why I have applied .foregroundCololor() to the VStack (with some images you might also have to add .renderingMode(.original) modifier):
struct ContentView: View {
#State private var showConnectionAlert = false
var body: some View {
Button(action: { self.showConnectionAlert = true }) {
VStack(spacing: 4) {
Image(systemName: "antenna.radiowaves.left.and.right")
Text("Connected")
} .foregroundColor(.primary)
}
.alert(isPresented: $showConnectionAlert) {
Alert(title: Text("Nice"),
message: Text("The alert is showing!"),
dismissButton: Alert.Button.default(Text("OK"),
action: { self.showConnectionAlert = false }))
}
}
}