Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed a bug with no view due to the SwiftUI view lifecycle. #184

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

HanweeeeLee
Copy link

@HanweeeeLee HanweeeeLee commented Nov 3, 2024

Problem 1

as-is)
The Partial Sheet does not work in the following code:

struct ContentView: View {
  var body: some View {
    VStack {
      
    }
    .partialSheet(isPresented: .constant(true)) {
      Text("Hello World")
    }
    .attachPartialSheetToRoot()
  }
}

The reason it doesn’t work can be identified in this code:

struct PSManagerWrapper<Parent: View, SheetContent: View>: View {
    ...
    var body: some View {
        parent
            .onChange(of: isPresented, perform: {_ in updateContent() })
    }
    ...
}

Since 'updateContent()' only triggers when the value of 'isPresented' changes, if the initial value is true, the screen does not update.

to-be)
Modifying the code as below allows the initial value to be reflected correctly:

struct PSManagerWrapper<Parent: View, SheetContent: View>: View {
    ...
    var body: some View {
        parent
            .onChange(of: isPresented, perform: {_ in updateContent() })
            .task { updateContent() }
    }
    ...
}

Problem 2

as-is)
The view disappears when the “Source of Truth” value changes, as shown below:

class SampleModel: ObservableObject {
  @Published var candidateChannelList: [String] = []
  
  func updateChannels() {
    candidateChannelList = ["Channel 1", "Channel 2", "Channel 3"]
  }
}

struct ContentView: View {
  @State private var isSheetPresented = false
  @StateObject private var sampleModel = SampleModel()
  
  var body: some View {
    VStack {
      Button("Show Partial Sheet") {
        isSheetPresented = true
      }
    }
    .partialSheet(isPresented: $isSheetPresented) {
      FeedChannelSelectorView(sampleModel: sampleModel, isPresented: $isSheetPresented)
    }
    .attachPartialSheetToRoot()
  }
}

struct FeedChannelSelectorView: View {
  @ObservedObject var sampleModel: SampleModel
  @Binding var isPresented: Bool
  
  var body: some View {
    VStack {
      HStack {
        Spacer()
        Button("Close") {
          isPresented = false
        }
        .padding()
      }
      
      Button(action: {
        sampleModel.updateChannels()
      }, label: {
        Text("RequestUpdate")
      })
      
      Text("Select a Channel")
        .font(.title)
        .padding(.bottom, 10)
      
      List(sampleModel.candidateChannelList, id: \.self) { channel in
        Text(channel)
          .onTapGesture {
            print("\(channel) selected")
            isPresented = false
          }
      }
    }
    .padding()
    .background(Color.white)
    .cornerRadius(10)
  }
}

We can see why the view disappears by looking at the following code:

public class PSManager: ObservableObject {
    
    /// Published var to present or hide the partial sheet
    @Published var isPresented: Bool = false {
        didSet {
            if !isPresented {
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { [weak self] in
                    self?.content = EmptyView().eraseToAnyView()
                    self?.onDismiss = nil
                }
            }
        }
    }
    ...

The PSManager class has its own @published property, isPresented. The Partial Sheet operates based on this isPresented value. When attachPartialSheetToRoot() is called, a new instance of PSManager is created:

public extension View {
    ...
    func attachPartialSheetToRoot() -> some View {
        let sheetManager: PSManager = PSManager()
        return self
            .modifier(PartialSheet())
            .environmentObject(sheetManager)
    }
    ...

Each time the “Source of Truth” changes, the view seems to be redrawn, causing a new PSManager to be created.

to-be)
To solve this, I modified attachPartialSheetToRoot() to allow an externally managed PSManager instance. Although this is not ideal, it provides a temporary solution:

public extension View {
    ...
    func attachPartialSheetToRoot(manager: PSManager? = nil) -> some View {
        let manager = {
           if let manager {
               return manager
           } else {
               return PSManager()
           }
        }() 
        
        let sheetManager: PSManager = manager
        
        return self
            .modifier(PartialSheet())
            .environmentObject(sheetManager)
    }
    ...

This requires making PSManager public to manage its instances externally.

public class PSManager: ObservableObject {
    ...
    public init() {
        content = EmptyView().eraseToAnyView()
        slideAnimation = PSSlideAnimation()
    }
    ...
}

Then, the ContentView is updated as follows:

class SampleModel: ObservableObject {
  @Published var candidateChannelList: [String] = []
  
  func updateChannels() {
    candidateChannelList = ["Channel 1", "Channel 2", "Channel 3"]
  }
}

struct ContentView: View {
  @State private var isSheetPresented = false
  @StateObject private var sampleModel = SampleModel()
  @StateObject private var psManager = PSManager()
  
  var body: some View {
    VStack {
      Button("Show Partial Sheet") {
        isSheetPresented = true
      }
    }
    .partialSheet(isPresented: $isSheetPresented) {
      FeedChannelSelectorView(sampleModel: sampleModel, isPresented: $isSheetPresented)
    }
    .attachPartialSheetToRoot(manager: psManager)
  }
}

struct FeedChannelSelectorView: View {
  @ObservedObject var sampleModel: SampleModel
  @Binding var isPresented: Bool
  
  var body: some View {
    VStack {
      HStack {
        Spacer()
        Button("Close") {
          isPresented = false
        }
        .padding()
      }
      
      Button(action: {
        sampleModel.updateChannels()
      }, label: {
        Text("Refresh")
      })
      
      Text("Select a Channel")
        .font(.title)
        .padding(.bottom, 10)
      
      List(sampleModel.candidateChannelList, id: \.self) { channel in
        Text(channel)
          .onTapGesture {
            print("\(channel) selected")
            isPresented = false
          }
      }
    }
    .padding()
    .background(Color.white)
    .cornerRadius(10)
  }
}

If you apply the above modifications, it should work as intended. Let me know if you have suggestions for a better approach!

정적인 뷰에서는 문제가 없지만, 구현상의 문제로 동적으로 데이터가 변하는 경우는 뷰가 사라짐.
이유는 Source of truth를 뷰의 생성자에서 생성하기 때문임. 
우선 임시로 Source of truth를 주입 받을 수 있게 수정해놓았음.
외부에서 PSManager(Source of truth)를 주입받기 위해서는 해당 클래스의 인스턴스를 직접 생성, 관리해야함. public으로 변경
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant