How can I send data from SwiftUI view to UIKit ViewController in callback's closure?
Let's say we have SwiftUI View:
import SwiftUI
struct MyView: View {
var buttonPressed: (() -> Void)?
#State var someData = ""
var body: some View {
ZStack {
Color.purple
Button(action: {
someData = "new Data"
self.buttonPressed?()
}) {
Text("Button")
}
}
}
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView()
}
}
And ViewController where, inside which we have SwiftUI view:
import UIKit
import SwiftUI
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let swiftUIView = MyView()
let hostingViewController = UIHostingController(rootView: swiftUIView)
self.view.addSubview(hostingViewController.view)
hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false
hostingViewController.view.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
hostingViewController.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
hostingViewController.view.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
hostingViewController.view.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true
hostingViewController.rootView.buttonPressed = {
print ("callback recived")
// i know i can try to get the data in this way, but if MyView become too complex than it won't look well
//print(hostingViewController.rootView.$someData)
}
}
}
How can I send someData via closure to ViewController?
You can pass it via argument, like
struct MyView: View {
var buttonPressed: ((String) -> Void)? // << here !!
#State var someData = ""
var body: some View {
ZStack {
Color.purple
Button(action: {
someData = "new Data"
self.buttonPressed?(someData) // << here !!
and
hostingViewController.rootView.buttonPressed = { value in // << here !!
print("callback received")
print(value)
}
Related
Fairly new to SwiftUI and trying to figure out how to use ViewModels. Coming from UIKit I tend to like binding button presses to view model events, then apply the business logic and return a new value.
I am trying this in SwiftUI:
struct MainView: View {
#ObservedObject private var viewModel: MainViewModel
#State private var isShowingBottomSheet = false
var body: some View {
VStack {
Text("Hello \(viewModel.username)")
.font(.title)
Button("Show bottom sheet") {
isShowingBottomSheet = true
}
.sheet(isPresented: $isShowingBottomSheet) {
let viewModel = SheetViewModel()
viewModel.event.usernameUpdated
.assign(to: &$viewModel.username)
SheetView(viewModel: viewModel)
.presentationDetents([.fraction(0.15), .medium])
}
}
}
// MARK: - Initializers
init(viewModel: MainViewModel) {
self.viewModel = viewModel
}
}
With the view model:
final class MainViewModel: ObservableObject {
// MARK: - Properties
#Published var username = "John"
}
And SheetView:
struct SheetView: View {
#ObservedObject private var viewModel: SheetViewModel
var body: some View {
VStack {
Text("Some Sheet")
.font(.title)
Button("Change Name") {
viewModel.event.updateUsernameButtonTapped.send(())
}
}
}
// MARK: - Initializers
init(viewModel: SheetViewModel) {
self.viewModel = viewModel
}
}
And SheetViewModel:
final class SheetViewModel: ObservableObject {
// MARK: - Events
struct Event {
let updateUsernameButtonTapped = PassthroughSubject<Void, Never>()
let usernameUpdated = PassthroughSubject<String, Never>()
}
// MARK: - Properties
let event = Event()
private var cancellables = Set<AnyCancellable>()
// MARK: - Binding
private func bindEvents() {
event.updateUsernameButtonTapped
.map { "Sam" }
.sink { [weak self] name in
self?.event.usernameUpdated.send(name)
}
.store(in: &cancellables)
}
}
I am getting the error Cannot convert value of type 'Binding<String>' to expected argument type 'Published<String>.Publisher'. I want my SheetViewModel to update the value of #Published var username in the MainViewModel. How would I go about this?
We usually don't need view model objects in SwiftUI which has a design that benefits from value semantics, rather than the more error prone reference semantics of UIKit. If you want to move logic out of the View struct you can group related state vars and mutating funcs in their own struct, e.g.
struct ContentView: View {
#State var config = SheetConfig()
var body: some View {
VStack {
Text(config.text)
Button(action: show) {
Text("Edit Text")
}
}
.sheet(isPresented: $config.isShowing,
onDismiss: didDismiss) {
TextField("Text", $config.text)
}
}
func show() {
config.show(initialText: "Hello")
}
func didDismiss() {
// Handle the dismissing action.
}
}
struct SheetConfig {
var text = ""
var isShowing = false
mutating func show(initialText: String) {
text = initialText
isShowing = true
}
}
If you want to persist/sync data, or use Combine then you will need to resort to the reference type version of state which is #StateObject. However if you use the new async/await and .task then it's possible to still not need it.
I'm making a class that returns its editor view.
But got an error
Cannot convert value of type 'Published.Publisher' to expected
argument type 'Binding'
This is simple test code for Playgrounds.
import SwiftUI
import PlaygroundSupport
struct EditorView: View {
#Binding var text: String
var body: some View {
HStack {
Text("Name:")
TextField("Jerry", text: $text)
}
}
}
class MyModel: ObservableObject {
#Published var name: String = "Tom"
func editorView() -> some View {
EditorView(text: $name) // [ERROR] Cannot convert value of type 'Published.Publisher' to expected argument type 'Binding<String>'
}
}
struct ContentView: View {
#StateObject var model: MyModel = .init()
var body: some View {
model.editorView()
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
.previewLayout(.fixed(width: 375, height: 400))
}
}
let viewController = UIHostingController(rootView: ContentView())
let nav = UINavigationController(rootViewController: viewController)
PlaygroundPage.current.liveView = nav
What is wrong?
2022.03.23 Added
I tested to make a view without binding, it works. I think that the class can create a view with its property. But can't pass the #Binding.
import SwiftUI
import PlaygroundSupport
struct EditorView: View {
#Binding var text: String
var body: some View {
HStack {
Text("Name:")
TextField("Jerry", text: $text)
}
}
}
// Just display the name parameter
struct DispView: View {
let name: String
var body: some View {
Text(name)
}
}
class MyModel: ObservableObject {
#Published var name: String = "Tom"
#ViewBuilder
func editorView() -> some View {
// EditorView(text: $name) // Error at this line.
DispView(name: name) // Pass the property.
}
}
struct ContentView: View {
#StateObject var model: MyModel = .init()
var body: some View {
model.editorView()
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
.previewLayout(.fixed(width: 375, height: 400))
}
}
let viewController = UIHostingController(rootView: ContentView())
let nav = UINavigationController(rootViewController: viewController)
PlaygroundPage.current.liveView = nav
You create EditorView in wrong place, put it in your ContentView instead of your ViewModel. ViewModel is for holding data (or model) instead of View. I believe SwiftUI is not designed for carrying view along.
struct ContentView: View {
#StateObject var model: MyModel = .init()
var body: some View {
editorView
}
}
extension ContentView {
#ViewBuilder
var editorView: some View {
EditorView(text: $model.name)
}
}
If you still want to put it in your ViewModel, this should be the working code. You can create new Binding as parameter for your view initializer.
#ViewBuilder
func editorView() -> some View {
EditorView(text:
Binding { name } set: { newName in self.name = newName }
)
}
Binding is provided by StateObject wrapper (on ObservableObject), and a view should be created in body (ie. in view hierarchy), so it should look like
struct ContentView: View {
#StateObject var model: MyModel = .init()
var body: some View {
EditorView(text: model.$name) // << here !!
}
}
Note: alternate would be to make EditorView dependent to MyModel directly and then inside such view you could create a wrapper which would provide needed binding.
I am using this for saving user info in user defaults for user isRegsitered.
struct UserKeys {
static let isUserRegistered = "isUserRegistered"
}
#propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
final class UserSettings: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
#UserDefault(UserKeys.isUserRegistered, defaultValue: false)
var isUserRegistered: Bool {
willSet {
objectWillChange.send()
}
}
}
// The Popup Login window on Same Dashboard View.
struct PopUpWindow: View {
#ObservedObject var userRepo = UserSettings()
Button(action: {
// Dismiss the PopUp
withAnimation(.linear(duration: 0.3)) {
userRepo.isUserRegistered = true
show = false
hideKeyboard()
}
}, label: {
Text("Submit")
& further I am listing in DashBoardView View as -
struct DashBoardView: View {
#ObservedObject var userRepo = UserSettings()
private var content: some View {
if !userRepo.isUserRegistered {
return PopUpWindow(show: .constant(true)) .eraseToAnyView()
}
// .. Other conditions ..
So basically I have popup with Login box so after submit I am trying to dismiss dialog & want to listen value userRepo.isUserRegistered
By initializing UserSettings class independently in both PopUpWindow and DashBoardView, you are actually saying that each view is managing its own data, hence the two views are not sharing the same data.
You should not initialize your UserSettings class from the PopUpWindow view, but instead, pass an instance to from DashBoardView.
The code would look like this:
PopUpWindow
struct PopUpWindow: View {
#ObservedObject var userRepo: UserSettings
Button(action: {
// Dismiss the PopUp
withAnimation(.linear(duration: 0.3)) {
userRepo.isUserRegistered = true
show = false
hideKeyboard()
}
}, label: {
Text("Submit")
// .. Other conditions ..
and then :
DashBoardView
struct DashBoardView: View {
#ObservedObject var userRepo = UserSettings()
private var content: some View {
if !userRepo.isUserRegistered {
return PopUpWindow(userRepo: userRepo, show: .constant(true)) .eraseToAnyView()
}
// .. Other conditions ..
I've included stubbed code samples. I'm not sure how to get this presentation to work. My expectation is that when the sheet presentation closure is evaluated, aDependency should be non-nil. However, what is happening is that aDependency is being treated as nil, and TheNextView never gets put on screen.
How can I model this such that TheNextView is shown? What am I missing here?
struct ADependency {}
struct AModel {
func buildDependencyForNextExperience() -> ADependency? {
return ADependency()
}
}
struct ATestView_PresentationOccursButNextViewNotShown: View {
#State private var aDependency: ADependency?
#State private var isPresenting = false
#State private var wantsPresent = false {
didSet {
aDependency = model.buildDependencyForNextExperience()
isPresenting = true
}
}
private let model = AModel()
var body: some View {
Text("Tap to present")
.onTapGesture {
wantsPresent = true
}
.sheet(isPresented: $isPresenting, content: {
if let dependency = aDependency {
// Never executed
TheNextView(aDependency: dependency)
}
})
}
}
struct TheNextView: View {
let aDependency: ADependency
init(aDependency: ADependency) {
self.aDependency = aDependency
}
var body: some View {
Text("Next Screen")
}
}
This is a common problem in iOS 14. The sheet(isPresented:) gets evaluated on first render and then does not correctly update.
To get around this, you can use sheet(item:). The only catch is your item has to conform to Identifiable.
The following version of your code works:
struct ADependency : Identifiable {
var id = UUID()
}
struct AModel {
func buildDependencyForNextExperience() -> ADependency? {
return ADependency()
}
}
struct ContentView: View {
#State private var aDependency: ADependency?
private let model = AModel()
var body: some View {
Text("Tap to present")
.onTapGesture {
aDependency = model.buildDependencyForNextExperience()
}
.sheet(item: $aDependency, content: { (item) in
TheNextView(aDependency: item)
})
}
}
I have ViewModel which looks like this:
class ItemsListViewModel : ObservableObject{
var response : ItemsListResponse? = nil
var itemsList : [ListItem] = []
var isLoading = true
let objectWillChange = PassthroughSubject<Void, Never>()
func getItems() {
self.isLoading = true
ApiManager.shared.getItems()
.sink(receiveCompletion: {completion in
}, receiveValue: {
self.response = data
self.isLoading = false
self.objectWillChange.send()
})
}
}
When I receive data from network request I use self.objectWillChange.send() to notify view, but view do not react to this.
My views :
ItemsView
struct ItemsView: View {
var body: some View {
VStack{
Text("Some Title")
ItemsListView()
}
}
}
ItemsListView
struct ItemsListView: View {
#ObservedObject var myViewModel = ItemsListViewModel()
var body: some View {
VStack{
Text("\(self.myViewModel.response?.total")
}.onAppear{
self.myViewModel.getItems()
}
}
}
But the interesting thing, that if I use ItemsListView not inside
ItemsView everything works perfectly. How can i solve this problem?
try this ( i simplified just your model to test in playground)
you can copy directly the code in playground and check
struct Model {
var items : [String]
}
class ItemsListViewModel : ObservableObject {
#Published var items : [String] = ["Test 1", "Test2"]
}
let myViewModel = ItemsListViewModel()
struct ItemsView: View {
var body: some View {
VStack{
Text("Some Title")
ItemsListView().environmentObject(myViewModel)
}
}
}
struct ItemsListView: View {
#EnvironmentObject private var model : ItemsListViewModel
var body: some View {
VStack{
Text("\(model.items.count)")
}
}
}
struct ContentView: View {
var body: some View {
ItemsView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(myViewModel)
}
}
Using #Published for properties of ObservableObject does fix your issue. See simplified below demo:
import SwiftUI
import Combine
class ItemsListViewModel : ObservableObject{
#Published var response = ""
var isLoading = true
func getData() {
self.isLoading = true
DispatchQueue.main.async {
Just("test")
.sink(receiveCompletion: {completion in
}, receiveValue: { data in
self.response = data
self.isLoading = false
})
}
}
}
struct ItemsListView: View {
#ObservedObject var myViewModel = ItemsListViewModel()
var body: some View {
VStack{
Text("\(self.myViewModel.response)")
}.onAppear{
self.myViewModel.getData()
}
}
}
struct ItemsView: View {
var body: some View {
VStack{
Text("Some Title")
ItemsListView()
}
}
}
struct TestPublished: View {
var body: some View {
ItemsView()
}
}
struct TestPublished_Previews: PreviewProvider {
static var previews: some View {
TestPublished()
}
}