I have an iOS project built with Storyboard and UIKit. Now I want to develop the new screens using SwiftUI. I added a Hosting View Controller to the existing Storyboard and used it to show my newly created SwiftUI view.
But I couldn't figure out how to create an #EnvironmenetObject that can be used anywhere throughout the application. I should be able to access/set it in any of my UIKit based ViewController as well as my SwiftUI views.
Is this possible? If so how to do it? In a pure SwiftUI app, we set the environment object like below,
#main
struct myApp: App {
#StateObject var item = Item()
var body: some Scene {
WindowGroup {
MainView()
.environmentObject(item)
}
}
}
But in my case, there is no function like this since it is an existing iOS project with AppDelegate and SceneDelegate. And the initial view controller is marked in Storyboard.
How to set this and access the object anywhere in the app?
The .environmentObject modifier changes the type of the view from ItemDetailView to something else. Force casting it will cause an error. Instead, try wrapping it into an AnyView.
class OrderObservable: ObservableObject {
#Published var order: String = "Hello"
}
struct ItemDetailView: View {
#EnvironmentObject var orderObservable: OrderObservable
var body: some View {
EmptyView()
.onAppear(perform: {
print(orderObservable.order)
})
}
}
class ItemDetailViewHostingController: UIHostingController<AnyView> {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
required init?(coder: NSCoder) {
super.init(coder: coder,rootView: AnyView(ItemDetailView().environmentObject(OrderObservable())))
}
}
This works for me. Is this what you require?
EDIT:
Ok, so I gave the setting the property from a ViewController all through the View. It wasn't as easy as using a property wrapper or a view modifier, but it works. I gave it a spin. Please let me know if this satisfies your requirement. Also, I had to get rid of the HostingController subclass.
class ViewController: UIViewController {
var orderObservable = OrderObservable()
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let myVC = (segue.destination as? MyViewController) else { return }
myVC.orderObservable = orderObservable
}
}
class MyViewController: UIViewController {
var orderObservable: OrderObservable!
var anycancellables = Set<AnyCancellable>()
#IBAction #objc func buttonSegueToHostingVC() {
let detailView = ItemDetailView().environmentObject(orderObservable)
present(UIHostingController(rootView: detailView), animated: true)
orderObservable.$order.sink { newVal in
print(newVal)
}
.store(in: &anycancellables)
}
}
class OrderObservable: ObservableObject {
#Published var order: String = "Hello"
init() {
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
self.order = "World"
}
}
}
struct ItemDetailView: View {
#EnvironmentObject var orderObservable: OrderObservable
var body: some View {
Text("\(orderObservable.order)")
}
}
Basically I'm creating the observable object in the ViewController class, passing it to the MyViewController class and finally create a hosting controller with the ItemDetailView and setting it's environmentObject and presenting it.
Here's my take on tackling this problem. My app targets iOS 14 or above:
The current state
I have a Main.storyboard file with one view controller scene set as the initial view controller with custom class ViewController. Here's the custom class implementation:
import UIKit
class ViewController: UIViewController {
#IBOutlet var label: UILabel!
}
The goal
To use this class in a SwiftUI app life cycle and make it react and interact to #EnvironmentObject instance (In this case let's call it a theme manager).
Solution
I will define a ThemeManager observable object with a Theme published property like so:
import SwiftUI
class ThemeManager: ObservableObject {
#Published var theme = Theme.purple
}
struct Theme {
let labelColor: Color
}
extension Theme {
static let purple = Theme(labelColor: .purple)
static let green = Theme(labelColor: .green)
}
extension Theme: Equatable {}
Next, I created a ViewControllerRepresentation to be able to use the ViewController in SwiftUI:
import SwiftUI
struct ViewControllerRepresentation: UIViewControllerRepresentable {
#EnvironmentObject var themeManager: ThemeManager
// Use this function to pass the #EnvironmentObject to the view controller
// so that you can change its properties from inside the view controller scope.
func makeUIViewController(context: Context) -> ViewController {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyboard.instantiateInitialViewController { coder in
ViewController(themeManager: themeManager, coder: coder)
}
return viewController!
}
// Use this function to update the view controller when the #EnvironmentObject changes.
// In this case I modify the label color based on the themeManager.
func updateUIViewController(_ uiViewController: ViewController, context: Context) {
uiViewController.label.textColor = UIColor(themeManager.theme.labelColor)
}
}
I then updated ViewController to accept a themeManager instance:
import UIKit
class ViewController: UIViewController {
#IBOutlet var label: UILabel!
let themeManager: ThemeManager
init?(themeManager: ThemeManager, coder: NSCoder) {
self.themeManager = themeManager
super.init(coder: coder)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#IBAction func toggleTheme(_ sender: UIButton) {
if themeManager.theme == .purple {
themeManager.theme = .green
} else {
themeManager.theme = .purple
}
}
}
Now, the last thing to do is create an instance of the theme manager and pass it as an environment object to the view controller representation:
import SwiftUI
#main
struct ThemeEnvironmentApp: App {
#StateObject private var themeManager = ThemeManager()
var body: some Scene {
WindowGroup {
ViewControllerRepresentation()
.environmentObject(themeManager)
}
}
}
Running the app shows our view controller with a label and a button. Tapping the button triggers the IBAction, which changes the themeManager.theme, which triggers a call to the representation's updateUIViewController(_:, context:):
Related
This is the first time I am working with SwiftUI, and I have searched for this problem but did not find any working solution, so I would appreciate all help I can get.
I have a UIViewController, in which I present a SwiftUI View through UIHostingController. What I want to achieve is that when I press the button in my SwiftUI view, the action is going to trigger the delegate in UIViewController, alternatively, trigger a function in UIViewController and then from that function trigger the delegate.
class MyViewController: UIViewController {
public var delegate: MyViewControllerDelegate?
let facialView = UIHostingController(rootView: FacialTutorial())
override func viewWillAppear(_ animated: Bool) {
addChild(facialView)
view.addSubview(facialView.view)
setupConstraints()
}
extension MyViewController {
#objc private func buttonPressed() {
delegate?.buttonPressed()
}
}
}
And in my SwiftUI view FacialTutorial
struct FacialTutorial: View {
var body: some View {
VStack() {
Button {
// I want to call delegate?.buttonPressed() action here
} label: {
Text("Press button")
}
}
}
}
EDIT
Okay to be more clear, my ViewController is configuring the page differently for a number of cases. So in practice, I do not initiate the SwiftUI view from viewWillAppear. Rather this is how I do
public var delegate: MyViewControllerDelegate?
let facialView = UIHostingController(rootView: FacialTutorial())
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
addChild(facialView)
configureSubviews()
}
private func configureForInstructionMode() {
view.addSubview(facialUIView.view)
setupConstraints()
}
I must have it this way because I need to configure the view differently depending on which mode I am going to configure for. When I declare the facialView inside the viewDidLoad or viewWillAppear, I cannot access the instance in configureForInstructionMode(), or it's value is nil..
You simply need to pass a reference to your MyViewController instance through to MyUIView (Which should probably be MyView since it isn't a subclass of UIView) via its initialiser. You could use a delegation pattern or you could pass a closure and then invoke the delegate method from the closure. The second way is more "Swifty".
(I have moved the code to viewDidLoad since if you have it in viewWillAppear the view may be added multiple times if the view controller appears and disappears and appears again)
class MyViewController: UIViewController {
public var delegate: MyViewControllerDelegate? {
weak var myView: UIHostingController<MyUIView>!
override func viewDidLoad() {
super.viewDidLoad()
let myView = UIHostingController(rootView: MyUIView(buttonHandler: { [weak self] in
self?.delegate?.buttonPressed()
}))
self.myView = myView
view.addSubview(myView.view)
setupConstraints()
}
}
Then, in your SwiftUI view you can declare a property to hold the closure and invoke that closure in your button
struct MyUIView: View {
var buttonHandler: (()->Void)?
var body: some View {
VStack() {
Button {
buttonHandler?()
} label: {
Text("Press button")
}
}
}
}
I'm looking for a way to change the text color of the status bar that allows a different text color to be used for each view.
I've seen this Q&A, but it's not what I'm looking for. I'm not looking for solutions that only allow for one status bar text color for all views. I want to change the status bar text color for each view. For example, one view might have a dark background and so I need light text. I might navigate to another view with a light background, so now I need dark text. The suggested duplicate answer only returns .lightContent, which means that the status bar text color cannot change dynamically when I move to a different view.
This answer here works on my machine, but it's not performant. A comment under it corroborates this. The lag is unacceptable, so this solution is not good.
Other solutions I've seen so far cause this particular error:
Compiling failed: extensions of generic classes cannot contain '#objc' members
I've also tried using an Environment Object inside my Custom Controller:
import SwiftUI
/// Allows for the status bar colors to be changed from black to white on the dark gray title bar
class Controller<ContentView> : UIHostingController<ContentView> where ContentView : View {
#EnvironmentObject var statusBarTextColor: StatusBarTextColor
lazy var isDark: Bool = self.statusBarTextColor.isDark
override var preferredStatusBarStyle: UIStatusBarStyle {
return isDark ? .lightContent : .darkContent
}
}
This results in the error:
Thread 1: Fatal error: No ObservableObject of type StatusBarTextColor found. A View.environmentObject(_:) for StatusBarTextColor may be missing as an ancestor of this view.
Inside my SceneDelegate file, I do specify the StatusBarTextColor environmentObject:
window.rootViewController = Controller(
rootView: Home()
.environmentObject(PostData())
.environmentObject(CardPosition())
.environmentObject(StatusBarTextColor())
)
And this is the ObservableObject itself:
import Combine
import SwiftUI
final class StatusBarTextColor: ObservableObject {
#Published var isDark: Bool = true
}
If I were to guess why this doesn't work, I'd say it's because the Controller gets initialized before StatusBarTextColor is available.
The more I look into this problem, the more I think there isn't a solution. I've gone through just about every article, answer, and video on the subject. They all either use a Controller to only return .lightContent, or use storyboards and multiple controllers, which isn't what I'm using.
You can use the solution you found here, but instead of using onDisappear, which will have a delay for the color change until the view is completely gone, you can create a view modifier called onWillDisappear that exposes viewWillDisappear. The color change will happen as sooner.
Usage:
struct MyClass: View {
#Environment(\.localStatusBarStyle) var statusBarStyle
// ...
SomeView()
}.onAppear {
self.statusBarStyle.currentStyle = .darkContent
}
.onWillDisappear {
self.statusBarStyle.currentStyle = .lightContent
}
}
Code:
import SwiftUI
class HostingController<Content>: UIHostingController<Content> where Content: View {
private var internalStyle = UIStatusBarStyle.lightContent
#objc override dynamic open var preferredStatusBarStyle: UIStatusBarStyle {
get {
internalStyle
}
set {
internalStyle = newValue
self.setNeedsStatusBarAppearanceUpdate()
}
}
override init(rootView: Content) {
super.init(rootView:rootView)
LocalStatusBarStyleKey.defaultValue.getter = { self.preferredStatusBarStyle }
LocalStatusBarStyleKey.defaultValue.setter = { self.preferredStatusBarStyle = $0 }
}
#objc required dynamic init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
class LocalStatusBarStyle { // style proxy to be stored in Environment
fileprivate var getter: () -> UIStatusBarStyle = { .default }
fileprivate var setter: (UIStatusBarStyle) -> Void = {_ in}
var currentStyle: UIStatusBarStyle {
get { self.getter() }
set { self.setter(newValue) }
}
}
// Custom Environment key, as it is set once, it can be accessed from anywhere
// of SwiftUI view hierarchy
struct LocalStatusBarStyleKey: EnvironmentKey {
static let defaultValue: LocalStatusBarStyle = LocalStatusBarStyle()
}
extension EnvironmentValues { // Environment key path variable
var localStatusBarStyle: LocalStatusBarStyle {
get {
return self[LocalStatusBarStyleKey.self]
}
}
}
struct WillDisappearHandler: UIViewControllerRepresentable {
func makeCoordinator() -> WillDisappearHandler.Coordinator {
Coordinator(onWillDisappear: onWillDisappear)
}
let onWillDisappear: () -> Void
func makeUIViewController(context: UIViewControllerRepresentableContext<WillDisappearHandler>) -> UIViewController {
context.coordinator
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<WillDisappearHandler>) {
}
typealias UIViewControllerType = UIViewController
class Coordinator: UIViewController {
let onWillDisappear: () -> Void
init(onWillDisappear: #escaping () -> Void) {
self.onWillDisappear = onWillDisappear
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
onWillDisappear()
}
}
}
struct WillDisappearModifier: ViewModifier {
let callback: () -> Void
func body(content: Content) -> some View {
content
.background(WillDisappearHandler(onWillDisappear: callback))
}
}
extension View {
func onWillDisappear(_ perform: #escaping () -> Void) -> some View {
self.modifier(WillDisappearModifier(callback: perform))
}
}
See original post with onWillDisappear code here
In your SceneDelegate you inject StatusBarTextColor() to the Home view. However, you declared the EvironmentObject in Controller.
I've been experimenting with SwiftUI and UIKit, trying to understand how data is shared between the two frameworks, and I've created a simple example for a larger project I am working on. The example is a single SwiftUI view that contains a UIViewControllerRepresentatable wrapping a custom view controller. I am trying to have the SwiftUI view display the value of one of the view controller's properties, but it does not refresh correctly when the value is changed.
struct ContentView: View {
#State var viewController = MyViewControllerRepresentable()
var body: some View {
VStack {
viewController
Text("super special property: \(viewController.viewController.data)")
}
}
}
class MyViewController: UIViewController, ObservableObject {
#Published var data = 3
override func viewDidLoad() {
let button = UIButton(type: .system)
button.setTitle("Increase by 1", for: .normal)
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
view = button
}
#objc func buttonPressed() {
data += 1
}
}
struct MyViewControllerRepresentable: UIViewControllerRepresentable {
#ObservedObject var viewController = MyViewController()
func makeUIViewController(context: Context) -> UIViewController {
return self.viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
When I run the app and press the button, I can see that the actual value of data is changing, and the publisher in MyViewController is firing, but the value displayed on screen is not refreshed to reflect this.
Please note, I am very new to iOS development, and this is probably an unconventional data model. However, I don't see why it shouldn't work correctly. Suggestions for a better way to share data would be much appreciated, but I would primarily like to know if it is possible to get this working with its current data structure.
Thank you in advance.
You could create a #Binding. This means that when the value is updated for data, the views are recreated to reflect the changes.
Here is how it can be done:
struct ContentView: View {
#State private var data = 3
var body: some View {
VStack {
MyViewControllerRepresentable(data: $data)
Text("super special property: \(data)")
}
}
}
class MyViewController: UIViewController {
#Binding private var data: Int
init(data: Binding<Int>) {
self._data = data
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
let button = UIButton(type: .system)
button.setTitle("Increase by 1", for: .normal)
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
view = button
}
#objc func buttonPressed() {
data += 1
}
}
struct MyViewControllerRepresentable: UIViewControllerRepresentable {
#Binding var data: Int
private let viewController: MyViewController
init(data: Binding<Int>) {
self._data = data
viewController = MyViewController(data: data)
}
func makeUIViewController(context: Context) -> UIViewController {
viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
In SwiftUI if you are transitioning using a NavigationLink() into a UIViewControllerRepresentable how would you; say, add buttons or change the title property on the navigationbar.
This is what I am doing right now:
import SwiftUI
/// Controls the actual action performed by the button upon taps.
struct CatagoryButton: View {
#State var isPresenting :Bool = false
var company : Company?
var text : String
var body: some View {
NavigationLink(destination: UIKitWrapper(company: self.company, storyboardPointer: self.text)
.navigationBarTitle(self.text)
.edgesIgnoringSafeArea(.all),
isActive: self.$isPresenting,
label: {
Button(action: {
self.isPresenting.toggle()
}){
ZStack {
ButtonShadowLayer(text: text)
GradientBackground()
.mask(ButtonBaseLayer())
CircleAndTextLayer(text: text)
}
}
})
}
}
Here is the struct for my representable.
import SwiftUI
/// Wraps UIKIT instance in a representable that swiftUI can present.
struct UIKitWrapper: UIViewControllerRepresentable {
//Specify what type of controller is being wrapped in an associated type.
typealias UIViewControllerType = UIViewController
//Company property passed from parent view. Represents the company the user selected from main view.
private var company : Company
//Determines which viewcontroller will be presented to user. This string corresponds to the name of the storyboard file in the main bundle.
private var storyboardPointer : String
init(company: Company?, storyboardPointer: String) {
guard let company = company else {fatalError()}
self.company = company
self.storyboardPointer = storyboardPointer
}
func makeUIViewController(context: Context) -> UIViewControllerType {
//Find user defined storyboard in bundle using name.
let storyboard = UIStoryboard(name: storyboardPointer, bundle: .main)
//Downcast returned controller to protocol AccessControllerProtocol. This step is required because we are not sure which storyboard will be accessed. Potential storyboard controllers that can be called all conform to this protocol.
//FIXME: Remove fatalError and create error enum asap.
guard let viewController = storyboard.instantiateInitialViewController() as? AccessControllerProtocol else { fatalError() }
//Assign user selected company object to instance property on incoming viewController.
viewController.company = company
//Return UINavigationController with storyboard instance view controller as root controller.
return viewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
Finally, here is one of the classes that use the representable.
import UIKit
class OrdersViewController: UIViewController, AccessControllerProtocol {
var company : Company!
#IBOutlet var companyNameLabel : UILabel!
override func viewDidLoad() {
super.viewDidLoad()
setBackgroundColor()
companyNameLabel.text = company.name
self.navigationController?.navigationItem.rightBarButtonItems = [UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(self.tapRightBarButton))]
}
func setBackgroundColor(){
let backgroundGradient = BackgroundGradientSetter()
let viewWithGradient = backgroundGradient.setGradientToView(with: [DarkBlueHue_DEFAULT,LightBlueHue_DEFAULT], size: view.bounds)
view.addSubview(viewWithGradient)
view.sendSubviewToBack(viewWithGradient)
}
#objc func tapRightBarButton(){
}
}
No matter what I do this button doesn't show up. I'm not sure if I need to put this in a makeCoordinator() or if there is just something I am missing. If anyone has insight please let me know!
If it isn't available in viewDidLoad, try calling your setupNavigation() in viewWillAppear()
In your case navigationController is not available yet on viewDidLoad, try instead as in below demo module
Tested & works with Xcode 11.2 / iOS 13.2
class MyUIController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.navigationController?.navigationBar.topItem?.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(self.onAdd(_:)))
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// might be needed to remove injected item here
}
#objc func onAdd(_ sender: Any?) {
print(">> tapped add")
}
}
struct MyInjector: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<MyInjector>) -> MyUIController {
MyUIController()
}
func updateUIViewController(_ uiViewController: MyUIController, context: UIViewControllerRepresentableContext<MyInjector>) {
}
}
struct DemoNavigationBarUIButton: View {
var body: some View {
NavigationView {
MyInjector()
.navigationBarTitle("Demo")
}
}
}
struct DemoNavigationBarUIButton_Previews: PreviewProvider {
static var previews: some View {
DemoNavigationBarUIButton()
}
}
I'm trying to call a local ViewController function from ContentView. The function uses some local variables and cannot be moved outside the ViewController.
class ViewController: UIViewController {
func doSomething() {...}
}
extension ViewController : LinkViewDelegate {...}
located on a different file:
struct ContentView: View {
init() {
viewController = .init(nibName:nil, bundle:nil)
}
var viewController: viewController
var body: some View {
Button(action: {self.viewController.doSomething()}) {
Text("Link Account")
}
}
}
UIViewController cannot be changed to something like UIViewRepresentable because LinkViewDelegate can only extend UIViewController.
So you need to create a simple bool binding in SwiftUI, flip it to true to trigger the function call in the UIKit viewController, and then set it back to false until the next time the swiftUI button is pressed. (As for LinkViewDelegate preventing something like UIViewControllerRepresentable that shouldn't stop you, use a Coordinator to handle the delegate calls.)
struct ContentView: View {
#State var willCallFunc = false
var body: some View {
ViewControllerView(isCallingFunc: $willCallFunc)
Button("buttonTitle") {
self.willCallFunc = true
}
}
}
struct ViewControllerView: UIViewControllerRepresentable {
#Binding var isCallingFunc: Bool
func makeUIViewController(context: Context) -> YourViewController {
makeViewController(context: context) //instantiate vc etc.
}
func updateUIViewController(_ uiViewController: YourViewController, context: Context) {
if isCallingFunc {
uiViewController.doSomething()
isCallingFunc = false
}
}
}
Here is a way that I've come up with which doesn't result in the "Modifying state during view update, this will cause undefined behavior" problem. The trick is to pass a reference of your ViewModel into the ViewController itself and then reset the boolean that calls your function there, not in your UIViewControllerRepresentable.
public class MyViewModel: ObservableObject {
#Published public var doSomething: Bool = false
}
struct ContentView: View {
#StateObject var viewModel = MyViewModel()
var body: some View {
MyView(viewModel: viewModel)
Button("Do Something") {
viewModel.doSomething = true
}
}
}
struct MyView: UIViewControllerRepresentable {
#ObservedObject var viewModel: MyViewModel
func makeUIViewController(context: Context) -> MyViewController {
return MyViewController(viewModel)
}
func updateUIViewController(_ viewController: MyViewController, context: Context) {
if viewModel.doSomething {
viewController.doSomething()
// boolean will be reset in viewController
}
}
}
class MyViewController: UIViewController {
var viewModel: MyViewModel
public init(_ viewModel: MyViewModel) {
self.viewModel = viewModel
}
public func doSomething() {
// do something, then reset the flag
viewModel.doSomething = false
}
}
You could pass the instance of ViewController as a parameter to ContentView:
struct ContentView: View {
var viewController: ViewController // first v lowercase, second one Uppercase
var body: some View {
Button(action: { viewController.doSomething() }) { // Lowercase viewController
Text("Link Account")
}
}
init() {
self.viewController = .init(nibName:nil, bundle:nil) // Lowercase viewController
}
}
// Use it for the UIHostingController in SceneDelegate.swift
window.rootViewController = UIHostingController(rootView: ContentView()) // Uppercase ContentView
Updated answer to better fit the question.