How to make a UIViewRepresentable #ViewBuilder work with dynamic content? - ios

Is it possible to make a UIViewRepresentable view which takes a ViewBuilder argument work with dynamic content such as ForEach loops?
I have the following UIViewRepresentable view which I’m using to drop down to UIKit and get some custom UIScrollView behaviour:
struct CustomScrollView<Content:View>: UIViewRepresentable {
private let content: UIView
private let scrollView = CustomUIScrollView()
init(#ViewBuilder content: () -> Content) {
self.content = UIHostingController(rootView: content()).view
self.content.backgroundColor = .clear
}
func makeUIView(context: Context) -> UIView {
scrollView.addSubview(content)
// ...
return scrollView
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
This works fine with static content as follows:
var body: some View {
CustomScrollView {
VStack {
ForEach(1..<50) { number in
Text(String(number))
}
}
}
}
But it fails with dynamic content, showing a blank view:
var body: some View {
CustomScrollView {
VStack {
ForEach(self.numbers) { number in
Text(String(number))
}
}
}
}
I understand that this is because when makeUIView() is called my dynamic data is empty, and it is later filled or updated. I evaluate my UIViewRepresentable’s content at init, and don’t update it in updateUIView().
How do you go about updating dynamic child content in updateUIView()? I tried capturing the #ViewBuilder parameter as an #escaping closure and evaluating it every time updateUIView() is called, which seems like the right solution (albeit inefficient?), but no luck so far.

Evaluation of #ViewBuilder fails because you mutating the wrong copy of the struct here
func updateUIView(_ uiView: UIView, context: Context) {}
You should mutate uiView's subviews directly with the content()
Updated Answer: Removed solution with a coordinator, because in some cases it works not as expected

The following might be helpful. It is not clear how absent CustomUIScrollView behaves (probably the issue is there), but using standard UIScrollView works with dynamic ForEach. Tested with Xcode 11.4 / iOS 13.4
struct CustomScrollView<Content:View>: UIViewRepresentable {
private let content: UIView
private let scrollView = UIScrollView()
init(#ViewBuilder content: () -> Content) {
self.content = UIHostingController(rootView: content()).view
self.content.backgroundColor = .clear
}
func makeUIView(context: Context) -> UIView {
content.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(content)
let constraints = [
content.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
content.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
content.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
content.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
content.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
]
scrollView.addConstraints(constraints)
return scrollView
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
struct TestCustomScrollView: View {
private var items = Array(repeating: "Test", count: 50)
var body: some View {
CustomScrollView {
VStack {
ForEach(Array(items.enumerated()), id: \.0) { i, item in
Text("\(item) - \(i)")
}
}
}
}
}

Related

Add modifier to all instances of view

I want to use my theme color on all instances of Toggle(), is there a way to do this with an extension?
extension Toggle {
func content() -> some View {
self.tint(.red)
}
}
The above is not working, is there something else I should call on the extension to modify all instances of Toggle?
This is exactly what .toggleStyle is designed for. Create your own custom ToggleStyle:
struct MyToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
// This just extends the default Toggle appearance, but you can return
// any View you like here. It doesn't have to call `Toggle` first.
Toggle(configuration)
.tint(.red) // Along with whatever other styles you like
}
}
extension ToggleStyle where Self == MyToggleStyle {
static var myToggleStyle: MyToggleStyle { .init() }
}
Then in your top-level ContentView, add the modifier:
.toggleStyle(.myToggleStyle)
Your style will be applied to all Toggles inside of your ContentView.
The best way to do this is to make a custom view with #ViewBuilder.
struct CustomToggle<Content: View>: View {
var isOn: Binding<Bool>
var label: Content
var body: some View {
Toggle(isOn: isOn) { label }
.tint(.red)
}
init(isOn: Binding<Bool>, #ViewBuilder label: #escaping () -> Content) {
self.isOn = isOn
self.label = label()
}
}
If you want to create a modifier to apply to an instance of Toggle(), can do that with the help of ViewModifiers.
i.e: First create a ViewModifier:
struct TintColorModifier: ViewModifier {
func body(content: Content) -> some View {
content
.tint(.red)
}
}
extension Toggle {
func tintStyle() -> some View {
modifier(TintColorModifier())
}
}
Now you can use the extension this way:
struct ContentView: View {
var body: some View {
Toggle()
.tintStyle() // <-- Here
}
}

UITextView inputAccessoryView not behaving properly on safe area phones?

I am trying use UIKit's UITextView in an swiftUI project using UIViewRepresentable. Everything works great but I am facing some issue with inputAccessoryView on phones with safe area.
My inputAccessoryView is a swiftUI view wrapped in UIHostingController which lets me use swiftUI views as UIView.
I have attached my code and some images, To simplify my issue I am using simple swiftUI colorView as inputAccessoryView. As you can see the swiftUI view moves out of the frame of inputAccessoryView when it comes near bottom safe area. When you swipe down to close the keyboard it disappears weirdly. I have tried everything but could not find the solution. Can you please help me?
Thank You!
UIViewRepresentable, UITextView
struct UITextViewWrapper: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.text = text
textView.inputAccessoryView = makeKeyboardBar()
textView.keyboardDismissMode = .interactive
textView.alwaysBounceVertical = true
textView.delegate = context.coordinator
textView.becomeFirstResponder()
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
//
}
class Coordinator: NSObject, UITextViewDelegate {
let parent: UITextViewWrapper
init(_ parent: UITextViewWrapper) {
self.parent = parent
}
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeKeyboardBar() -> UIView {
let swiftUIView = ColorView()
let hostingController = UIHostingController(rootView: swiftUIView)
let uiView = hostingController.view!
uiView.frame = CGRect(origin: .zero, size: hostingController.sizeThatFits(in: uiView.frame.size))
uiView.layer.borderWidth = 1
uiView.layer.borderColor = UIColor.red.cgColor
return uiView
}
}
SwiftUI View to use as inputAccessoryView
struct ColorView: View {
var body: some View {
Color.yellow
.frame(height: 55)
}
}
ContentView
struct ContentView: View {
#State private var text = "Hello World"
var body: some View {
UITextViewWrapper(text: $text)
}
}
image
SwiftUI has own safe area tracking so you have to manage safe area behaviour explicitly inside SwiftUI view, like
var body: some View {
Color.yellow.edgesIgnoringSafeArea(.bottom) // << here !!
.frame(height: 55)
}

Send data changes from UIKit, Wrapped inside UIViewRepresentable, to SwiftUI, and Rich Text Editor problem

I am working on a SwiftUI project, the functionalities it required is to make a Rich Text Editor on IOS.
The approach I am following is fairly simple, I used cbess/RichTextEditor link originally written in UIKit and import it into SwiftUI. To run the imported UIView, I wrap the view inside one UIViewRpresentable and add it into the ContentView struct of SwiftUI.
Now, I want to publish the data inside UIView and assign it to one of #state ContentView owns.
The code structure look similar to this:
For the ContentView (SwiftUI)
struct ContentView: View {
#State var textHtml: String = "" //I want all changes come from UIView be stored inside this
var body: some View {
VStack {
Cbess(
frameEditor: CGRect(x: 0, y: 40, width: 360, height: 400)
)
}
}
}
For the UiViewRepresentable
struct Cbess : UIViewRepresentable{
let frameEditor : CGRect
func makeUIView(context: Context) -> UIView {
let frameEditor = RichEditorView(frame: frameEditor)
let uiView : UIView = UIView()
uiView.addSubview(editorView)
return uiView
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
For the UiView(Simplified)
#objcMembers open class RichEditorView: UIView, {
var contentHTML : String // This variable get updated regularly
}
One additional question is that I want to make a Rich Text Editor by solely SwiftUI. How can I achieve it? Can you give me some keywords? Some Repo?
Any help is very appreciated! Thanks for read this whole question.
Use #Binding and delegate.
UIViewRepresentable view
struct Cbess : UIViewRepresentable {
#Binding var textHtml: String
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> RichEditorView {
let editorView = RichEditorView()
editorView.delegate = context.coordinator
return editorView
}
func updateUIView(_ uiView: RichEditorView, context: Context) {
}
class Coordinator: NSObject, RichEditorDelegate {
var parent: Cbess
init(_ parent: Cbess) {
self.parent = parent
}
// Use delegate here
func richEditor(_ editor: RichEditorView, contentDidChange content: String) {
self.parent.textHtml = content
print(content)
}
}
}
Your content view:
struct ContentView: View {
#State var textHtml: String = ""
var body: some View {
VStack {
Cbess(textHtml: $textHtml)
.frame(width: 360, height: 400)
Text("Print----\n\(textHtml)")
}
}
}

SwiftUI View not updating its state when embedded in UIView (using UIHostingController)

I'm wanting to use a SwiftUI View as content for a child UIView (which in my app would be inside UIViewController) by passing SwiftUI. However the SwiftUI View doesn't respond to state changes once embedded inside UIView.
I created the simplified version of my code below that has the issue.
When tapping the Text View embedded inside the EmbedSwiftUIView the outer Text View at the top VStack updates as expected but the Text View embedded inside the EmbedSwiftUIView does not update its state.
struct ProblemView: View {
#State var count = 0
var body: some View {
VStack {
Text("Count is: \(self.count)")
EmbedSwiftUIView {
Text("Tap to increase count: \(self.count)")
.onTapGesture {
self.count = self.count + 1
}
}
}
}
}
struct EmbedSwiftUIView<Content:View> : UIViewRepresentable {
var content: () -> Content
func makeUIView(context: UIViewRepresentableContext<EmbedSwiftUIView<Content>>) -> UIView {
let host = UIHostingController(rootView: content())
return host.view
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<EmbedSwiftUIView<Content>>) {
}
}
Update view or view controller in updateUIView or updateUIViewController function. In this case, using UIViewControllerRepresentable is easier.
struct EmbedSwiftUIView<Content: View> : UIViewControllerRepresentable {
var content: () -> Content
func makeUIViewController(context: Context) -> UIHostingController<Content> {
UIHostingController(rootView: content())
}
func updateUIViewController(_ host: UIHostingController<Content>, context: Context) {
host.rootView = content() // Update content
}
}
The only way is to create dedicated view with encapsulated increment logic:
struct IncrementView: View {
#Binding var count: Int
var body: some View {
Text("Tap to increase count: \(count)")
.onTapGesture {
count += 1
}
}
}
struct ProblemView: View {
#State var count = 0
var body: some View {
VStack {
Text("Count is: \(count)")
EmbedSwiftUIView {
IncrementView(count: $count)
}
}
}
}

Send tapAction from SwiftUI button action to UIView function

I'm trying to find a way to trigger an action that will call a function in my UIView when a button gets tapped inside swiftUI.
Here's my setup:
foo()(UIView) needs to run when Button(SwiftUI) gets tapped
My custom UIView class making use of AVFoundation frameworks
class SomeView: UIView {
func foo() {}
}
To use my UIView inside swiftUI I have to wrap it in UIViewRepresentable
struct SomeViewRepresentable: UIViewRepresentable {
func makeUIView(context: Context) -> CaptureView {
SomeView()
}
func updateUIView(_ uiView: CaptureView, context: Context) {
}
}
SwiftUI View that hosts my UIView()
struct ContentView : View {
var body: some View {
VStack(alignment: .center, spacing: 24) {
SomeViewRepresentable()
.background(Color.gray)
HStack {
Button(action: {
print("SwiftUI: Button tapped")
// Call func in SomeView()
}) {
Text("Tap Here")
}
}
}
}
}
You can store an instance of your custom UIView in your representable struct (SomeViewRepresentable here) and call its methods on tap actions:
struct SomeViewRepresentable: UIViewRepresentable {
let someView = SomeView() // add this instance
func makeUIView(context: Context) -> SomeView { // changed your CaptureView to SomeView to make it compile
someView
}
func updateUIView(_ uiView: SomeView, context: Context) {
}
func callFoo() {
someView.foo()
}
}
And your view body will look like this:
let someView = SomeViewRepresentable()
var body: some View {
VStack(alignment: .center, spacing: 24) {
someView
.background(Color.gray)
HStack {
Button(action: {
print("SwiftUI: Button tapped")
// Call func in SomeView()
self.someView.callFoo()
}) {
Text("Tap Here")
}
}
}
}
To test it I added a print to the foo() method:
class SomeView: UIView {
func foo() {
print("foo called!")
}
}
Now tapping on your button will trigger foo() and the print statement will be shown.
M Reza's solution works for simple situations, however if your parent SwiftUI view has state changes, every time when it refreshes, it will cause your UIViewRepresentable to create new instance of UIView because of this: let someView = SomeView() // add this instance. Therefore someView.foo() is calling the action on the previous instance of SomeView you created, which is already outdated upon refreshing, so you might not see any updates of your UIViewRepresentable appear on your parent view.
See: https://medium.com/zendesk-engineering/swiftui-uiview-a-simple-mistake-b794bd8c5678
A better practice would be to avoid creating and referencing that instance of UIView when calling its function.
My adaption to M Reza's solution would be calling the function indirectly through parent view's state change, which triggers updateUIView :
var body: some View {
#State var buttonPressed: Bool = false
VStack(alignment: .center, spacing: 24) {
//pass in the #State variable which triggers actions in updateUIVIew
SomeViewRepresentable(buttonPressed: $buttonPressed)
.background(Color.gray)
HStack {
Button(action: {
buttonPressed = true
}) {
Text("Tap Here")
}
}
}
}
struct SomeViewRepresentable: UIViewRepresentable {
#Binding var buttonPressed: Bool
func makeUIView(context: Context) -> SomeView {
return SomeView()
}
//called every time buttonPressed is updated
func updateUIView(_ uiView: SomeView, context: Context) {
if buttonPressed {
//called on that instance of SomeView that you see in the parent view
uiView.foo()
buttonPressed = false
}
}
}
Here's another way to do it using a bridging class.
//SwiftUI
struct SomeView: View{
var bridge: BridgeStuff?
var body: some View{
Button("Click Me"){
bridge?.yo()
}
}
}
//UIKit or AppKit (use NS instead of UI)
class BridgeStuff{
var yo:() -> Void = {}
}
class YourViewController: UIViewController{
override func viewDidLoad(){
let bridge = BridgeStuff()
let view = UIHostingController(rootView: SomeView(bridge: bridge))
bridge.yo = { [weak self] in
print("Yo")
self?.howdy()
}
}
func howdy(){
print("Howdy")
}
}
Here is yet another solution! Communicate between the superview and the UIViewRepresentable using a closure:
struct ContentView: View {
/// This closure will be initialized in our subview
#State var closure: (() -> Void)?
var body: some View {
SomeViewRepresentable(closure: $closure)
Button("Tap here!") {
closure?()
}
}
}
Then initialize the closure in the UIViewRepresentable:
struct SomeViewRepresentable: UIViewRepresentable {
// This is the same closure that our superview will call
#Binding var closure: (() -> Void)?
func makeUIView(context: Context) -> UIView {
let uiView = UIView()
// Since `closure` is part of our state, we can only set it on the main thread
DispatchQueue.main.async {
closure = {
// Perform some action on our UIView
}
}
return uiView
}
}
#ada10086 has a great answer. Just thought I'd provide an alternative solution that would be more convenient if you want to send many different actions to your UIView.
The key is to use PassthroughSubject from Combine to send messages from the superview to the UIViewRepresentable.
struct ContentView: View {
/// This will act as a messenger to our subview
private var messenger = PassthroughSubject<String, Never>()
var body: some View {
SomeViewRepresentable(messenger: messenger) // Pass the messenger to our subview
Button("Tap here!") {
// Send a message
messenger.send("button-tapped")
}
}
}
Then we monitor the PassthroughSubject in our subview:
struct SomeViewRepresentable: UIViewRepresentable {
let messenger = PassthroughSubject<String, Never>()
#State private var subscriptions: Set<AnyCancellable> = []
func makeUIView(context: Context) -> UIView {
let uiView = UIView()
// This must be run on the main thread
DispatchQueue.main.async {
// Subscribe to messages
messenger.sink { message in
switch message {
// Call funcs in `uiView` depending on which message we received
}
}
.store(in: &subscriptions)
}
return uiView
}
}
This approach is nice because you can send any string to the subview, so you can design a whole messaging scheme.
My solution is to create an intermediary SomeViewModel object. The object stores an optional closure, which is assigned an action when SomeView is created.
struct ContentView: View {
// parent view holds the state object
#StateObject var someViewModel = SomeViewModel()
var body: some View {
VStack(alignment: .center, spacing: 24) {
SomeViewRepresentable(model: someViewModel)
.background(Color.gray)
HStack {
Button {
someViewModel.foo?()
} label: {
Text("Tap Here")
}
}
}
}
}
struct SomeViewRepresentable: UIViewRepresentable {
#ObservedObject var model: SomeViewModel
func makeUIView(context: Context) -> SomeView {
let someView = SomeView()
// we don't want the model to hold on to a reference to 'someView', so we capture it with the 'weak' keyword
model.foo = { [weak someView] in
someView?.foo()
}
return someView
}
func updateUIView(_ uiView: SomeView, context: Context) {
}
}
class SomeViewModel: ObservableObject {
var foo: (() -> Void)? = nil
}
Three benefits doing it this way:
We avoid the original problem that #ada10086 identified with #m-reza's solution; creating the view only within the makeUIView function, as per the guidance from Apple Docs, which state that we "must implement this method and use it to create your view object."
We avoid the problem that #orschaef identified with #ada10086's alternative solution; we're not modifying state during a view update.
By using ObservableObject for the model, we can add #Published properties to the model and communicate state changes from the UIView object. For instance, if SomeView uses KVO for some of its properties, we can create an observer that will update some #Published properties, which will be propagated to any interested SwiftUI views.

Resources