(SwiftUI) Kingfisher ForEach issue - swiftui

I want to print web images using ForEach.
*To print web images, I used a 3rd party library, kingfisher.
import SwiftUI
import Kingfisher
struct KingFisherTest: View {
var body: some View {
VStack {
ForEach(0 ..< 3, id: \.self) { _ in
KFImage(URL(string: "https://cdn.imweb.me/upload/S2017101359e025984d346/bff36a6d2ced4.jpg"))
.resizable()
.frame(width: 150, height: 150)
.onAppear {
print("onAppear!")
}
.onDisappear {
print("onDisappear!")
}
}
}
}
}
struct KingFisherTest_Previews: PreviewProvider {
static var previews: some View {
KingFisherTest()
}
}
However, there is an issue where onAppear() occurs twice when one image is printed.
I want onAppear() to do a specific function, but the issue causes the function to work twice.
How can I fix this issue?

Related

Passing data across views for unique objects in a forEach loop in swift

I have two views, ViewAssignment and TaskDetailView. My ViewAssignment page fetches data from an environment object, and creates a list using the data.
Upon each item of the list being clicked on, the TaskDetailView pops in as a navigation link, however, I am having trouble making the information in the TaskDetailView unique to that particular iteration (the item in the list)
I believe the trouble comes from my TaskDetailView.swift
import SwiftUI
struct TaskDetailView: View {
#EnvironmentObject var assignment: Assignments
#State var taskNotes = ""
var body: some View {
VStack(spacing: 10) {
Image("english-essay")
.resizable()
.scaledToFit()
.frame(width: 250, height: 160)
.cornerRadius(20)
Text(self.assignment.data.first?.taskName ?? "Untitled Task")
.font(.title2)
.fontWeight(.semibold)
.lineLimit(2)
.multilineTextAlignment(.center)
HStack(spacing: 20) {
Label(self.assignment.data.first?.weighting ?? "0", systemImage: "percent")
.font(.subheadline)
.foregroundColor(.secondary)
Text(self.assignment.data.first?.dueDate ?? "No Date")
.font(.subheadline)
.foregroundColor(.secondary)
}
TextField("Write any notes here", text: $taskNotes)
.font(.body)
.padding()
Spacer()
}
}
}
struct TaskDetailView_Previews: PreviewProvider {
static var previews: some View {
TaskDetailView() // I assume there is some information I have to pass through here
}
}
For details, this is my other view:
import SwiftUI
struct ViewAssignment: View {
// Observed to update the UI
#EnvironmentObject var assignment: Assignments
var body: some View {
ZStack {
NavigationView {
List(self.assignment.data) { task in
NavigationLink (
destination: TaskDetailView(),
label: {
Image(systemName: "doc.append.fill")
.scaleEffect(2.5)
.padding()
VStack(alignment: .leading, spacing: 3) {
Text(task.taskName)
.fontWeight(.semibold)
.lineLimit(2)
Text(task.dueDate + " - " + task.subject)
.font(.subheadline)
.foregroundColor(.secondary)
}
})
}
.navigationTitle("My Tasks")
.listStyle(InsetGroupedListStyle())
}
}
}
}
struct ViewAssignment_Previews: PreviewProvider {
static var previews: some View {
ViewAssignment()
}
}
I would also like to know if, upon making the screen unique for each item in the list, would I be able to have the contents of the text field saved upon reloading the app, Perhaps through #AppStorage?
Thank you for the assistance.
If I understand correctly what you are trying to do:
a TaskDetailView displays the detail of a ... Task.
So you should have a Task structure like this:
struct Task {
let name: String
let subject: String
...
}
You have to create one (or more) instance of Task to test your TaskDetailView:
extension Task {
var test: Task {
Task(name: "Test", subject: "Test Subject")
}
}
Now in the preview of your TaskDetailView you can try to display your example :
struct TaskDetailView_Previews: PreviewProvider {
static var previews: some View {
TaskDetailView(task: Task.test) // here
}
}
For the moment nothing is happening. Because your TaskDetailView doesn't have a task parameter.
struct TaskDetailView: View {
var task: Task
var body: some View {
...
}
Now its body can use the different parameters of this Task.
Text(task.name)
.font(.title2)
.fontWeight(.semibold)
.lineLimit(2)
.multilineTextAlignment(.center)
Now in your List:
List(self.assignment.data) { task in
NavigationLink (
destination: TaskDetailView(task: task), // <- here !!!
label: {
Image(systemName: "doc.append.fill")
.scaleEffect(2.5)
.padding()
VStack(alignment: .leading, spacing: 3) {
Text(task.name)
.fontWeight(.semibold)
.lineLimit(2)
}
})
}

SwiftUI View appear differently in device and preview compare to view hierarchy capture

I have a problem with SwiftUI
There was a issue
yello view don't appear at first launch view
move to home that makes app into background mode
move to app (foreground)
then yellow view appears
I captured view hierarchy in step 1.
(left is view hierarchy capture, right is simulator)
view hierarchy capture shows the yellow square but simulator didn't show yellow square
I checked the view breadscrum but both was same so I have no clue.
I'm sure this is not a networking problem.
There is two way to appear yellow square
background -> foreground
present alert -> dismiss alert
I'm not sure this is a framework bug or else.
Also, is there any API that I can print the swiftUI rendering request succeed or fail?
Thank you in advance and merry christmas!
(left is before yello square appear on simulator/ right is after yello square appear on simulator)
edit - add sample code
contentView
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel : viewModel
#ObservedObject var params : otherViewModel
var body: some View {
HorizontalScrollView(viewModel: viewModel, someParmas: params)
.padding(.leading, 24)
.frame(width: UIScreen.main.bounds.width, height:400)
.background(Color.red)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(viewModel: viewModel(homeAPI: HomeAPI()), params: otherViewModel())
}
}
import SwiftUI
struct HorizontalScrollView: View {
#ObservedObject var viewModel: viewModel
#ObservedObject var holder : otherViewModel
private var homeHightlightRange: Range<Int> {
return 0..<(viewModel.something?.somethingList?.count ?? 0)
}
init(viewModel: viewModel, someParmas: otherViewModel) {
self.viewModel = viewModel
self.holder = someParmas
}
var body: some View {
VStack(spacing: 20) {
HStack(spacing: 0) {
Text("Merry christmas")
.font(.system(size: 24))
.foregroundColor(.white)
.bold()
.onTapGesture {
viewModel.getHighlight()
}
Spacer()
}
ScrollView(.horizontal, showsIndicators: false, content: {
HStack(alignment:.bottom, spacing: 14) {
ForEach(homeHightlightRange, id: \.self) { index in
Color.yellow.frame(width:200, height:300)
}
}
})
}
.frame(height: 339)
}
}
struct HighlightView_Previews: PreviewProvider {
static var previews: some View {
HorizontalScrollView(viewModel: viewModel(homeAPI: HomeAPI()),someParmas: otherViewModel())
.previewLayout(.sizeThatFits)
}
}
homeHightlightRange is got from server via viewModel
I suppose that the solution is to add ScrollView conditionally on API results appear, like (not tested - typed here, so typos might be present)
if viewModel.something?.somethingList?.count != 0 {
ScrollView(.horizontal, showsIndicators: false, content: {
HStack(alignment:.bottom, spacing: 14) {
ForEach(homeHightlightRange, id: \.self) { index in
Color.yellow.frame(width:200, height:300)
}
}
})
}

SwiftUI Navigation Controller stuttering with two Navigationlinks per List row

I am trying to create two NavigationLinks in a repeating List. Each has a separate destination. The code all works fine until I imbed the call to the root view in a List/ForEach loop. At which point the navigation becomes very strange.
Try to click on either link and then click the back indicator at the top. It will go to one NavigationLink, and then the other. Sometimes in a different order, and sometimes it will auto-return from one of the links, and othertimes it won't open the second detail view until you return from the first detail view. It does this both in Preview, as well as if you build and run the application.
I have distilled down the code to the most basic below. If you comment the 2 lines as indicated in ContentView, you will then see correct behavior.
I am running Catalina 10.15.5, xCode 11.6, with the application target of IOS 13.6.
How can I modify the code, so that it will work with the List/ForEach loop?
import SwiftUI
struct DetailView1: View {
var body: some View {
HStack {
Text("Here is Detail View 1." )}
.foregroundColor(.green)
}
}
struct DetailView2: View {
var body: some View {
HStack {
Text( "Here is Detail View 2.") }
.foregroundColor(.red)
}
}
struct RootView: View {
var body: some View {
HStack {
VStack {
NavigationLink(destination: DetailView1())
{ VStack { Image(systemName: "ant.circle").resizable()
.frame(width:75, height:75)
.scaledToFit()
}.buttonStyle(PlainButtonStyle())
Text("Tap for Detail 1.")
.foregroundColor(.green)
}
}
NavigationLink(destination: DetailView2())
{ Text("Tap for Detail 2.")
.foregroundColor(.red)
}
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
// Comment the following line for correct behavior
List { ForEach(0..<3) {_ in
RootView()
// Comment the following line for correct behavior
} }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
ContentView()
.navigationBarTitle("Strange Behavior")
}
}
}
In your case both navigation links are activated at once user tap a row, to avoid this below is possible approach
Tested with Xcode 12 / iOS 14
The idea is to have one link which is activated programmatically and destination is selected dynamically depending on which button is clicked
struct RootView: View {
#State private var isFirst = false
#State private var isActive = false
var body: some View {
HStack {
VStack {
Button(action: {
self.isFirst = true
self.isActive = true
})
{ VStack { Image(systemName: "ant.circle").resizable()
.frame(width:75, height:75)
.scaledToFit()
}
Text("Tap for Detail 1.")
.foregroundColor(.green)
}
}
Button(action: {
self.isFirst = false
self.isActive = true
})
{ Text("Tap for Detail 2.")
.foregroundColor(.red)
}
.background(
NavigationLink(destination: self.destination(), isActive: $isActive) { EmptyView() }
)
}
.buttonStyle(PlainButtonStyle())
}
#ViewBuilder
private func destination() -> some View {
if isFirst {
DetailView1()
} else {
DetailView2()
}
}
}

SwiftUI - Animation in view stops when scrolling the list

Considering the following code, why the animations in the views that are initialized without the n property stops when you scroll the list?
Tested on Xcode 11.3 (11C29) with a new default project on device and simulator.
import SwiftUI
struct ContentView: View {
var body: some View {
HStack {
List(1...50, id: \.self) { n in
HStack {
KeepRolling()
Spacer()
KeepRolling(n: n)
}
}
}
}
}
struct KeepRolling: View {
#State var isAnimating = false
var n: Int? = nil
var body: some View {
Rectangle()
.frame(width: 50, height: 50)
.rotationEffect(Angle(degrees: self.isAnimating ? 360 : 0))
.onAppear {
withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) {
self.isAnimating = true
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
IMO it is due to caching/reuse in List. For List all the values of KeepRolling() is the same, so .onAppear is not always re-called.
If to make every such view unique, say using .id as below, all works (tested with Xcode 11.2)
KeepRolling().id(UUID().uuidString)

SwiftUI: popover to persist (not be dismissed when tapped outside)

I created this popover:
import SwiftUI
struct Popover : View {
#State var showingPopover = false
var body: some View {
Button(action: {
self.showingPopover = true
}) {
Image(systemName: "square.stack.3d.up")
}
.popover(isPresented: $showingPopover){
Rectangle()
.frame(width: 500, height: 500)
}
}
}
struct Popover_Previews: PreviewProvider {
static var previews: some View {
Popover()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
Default behaviour is that is dismisses, once tapped outside.
Question:
How can I set the popover to:
- Persist (not be dismissed when tapped outside)?
- Not block screen when active?
My solution to this problem doesn't involve spinning your own popover lookalike. Simply apply the .interactiveDismissDisabled() modifier to the parent content of the popover, as illustrated in the example below:
import SwiftUI
struct ContentView: View {
#State private var presentingPopover = false
#State private var count = 0
var body: some View {
VStack {
Button {
presentingPopover.toggle()
} label: {
Text("This view pops!")
}.popover(isPresented: $presentingPopover) {
Text("Surprise!")
.padding()
.interactiveDismissDisabled()
}.buttonStyle(.borderedProminent)
Text("Count: \(count)")
Button {
count += 1
} label: {
Text("Doesn't block other buttons too!")
}.buttonStyle(.borderedProminent)
}
.padding()
}
}
Tested on iPadOS 16 (Xcode 14.1), demo video included below:
Note: Although it looks like the buttons have lost focus, they are still interact-able, and might be a bug as such behaviour doesn't exist when running on macOS.
I tried to play with .popover and .sheet but didn't found even close solution. .sheet can present you modal view, but it blocks parent view. So I can offer you to use ZStack and make similar behavior (for user):
import SwiftUI
struct Popover: View {
#State var showingPopover = false
var body: some View {
ZStack {
// rectangles only for color control
Rectangle()
.foregroundColor(.gray)
Rectangle()
.foregroundColor(.white)
.opacity(showingPopover ? 0.75 : 1)
Button(action: {
withAnimation {
self.showingPopover.toggle()
}
}) {
Image(systemName: "square.stack.3d.up")
}
ModalView()
.opacity(showingPopover ? 1: 0)
.offset(y: self.showingPopover ? 0 : 3000)
}
}
}
// it can be whatever you need, but for arrow you should use Path() and draw it, for example
struct ModalView: View {
var body: some View {
VStack {
Spacer()
ZStack {
Rectangle()
.frame(width: 520, height: 520)
.foregroundColor(.white)
.cornerRadius(10)
Rectangle()
.frame(width: 500, height: 500)
.foregroundColor(.black)
}
}
}
}
struct Popover_Previews: PreviewProvider {
static var previews: some View {
Popover()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
here ModalView pops up from below and the background makes a little darker. but you still can touch everything on your "parent" view
update: forget to show the result:
P.S.: from here you can go further. For example you can put everything into GeometryReader for counting ModalView position, add for the last .gesture(DragGesture()...) to offset the view under the bottom again and so on.
You just use .constant(showingPopover) instead of $showingPopover. When you use $ it uses binding and updates your #State variable when you press outside the popover and closes your popover. If you use .constant(), it will just read the value from you #State variable, and will not close the popover.
Your code should look like this:
struct Popover : View {
#State var showingPopover = false
var body: some View {
Button(action: {
self.showingPopover = true
}) {
Image(systemName: "square.stack.3d.up")
}
.popover(isPresented: .constant(showingPopover)) {
Rectangle()
.frame(width: 500, height: 500)
}
}
}