Updating SwiftUI view from an Objective C Class - ios

I am not experienced with IOS development but have some basic understanding to work my way through it by reading docs and tutorials.
I wanted to call Objective C code from Swift and it worked fine, now I want to do the opposite and getting confused a bit.
Basically I first call an Objective C function in the action of a Button in SwiftUI, then I want that function to update an ObservedObject in the same SwiftUI view and want the view to re-render.
I have found and followed a few resources on that, which are
https://medium.com/#iainbarclay/adding-swiftui-to-objective-c-apps-63abc3b26c33
https://pinkstone.co.uk/how-to-use-swift-classes-in-objective-c/
Swift UI view looks like
class Foo : ObservableObject {
#Published var bar = ""
}
struct ContentView: View {
#ObservedObject var baz = Foo();
// Then access later as self.baz.bar as a parameter somewhere..
What would be the right way to update bar here ?
I did the correct build settings and added #objc tags and also imported project_name-swift.h.
Implemented and modified the example in
https://medium.com/#iainbarclay/adding-swiftui-to-objective-c-apps-63abc3b26c33 but got lost a bit because of my lack of experience in these environments.
Maybe somebody can push me in the right direction.
Thank you.
Let's assume my project name is Project.
Example code :
(A code very similar to this, compiles fine and the Objective C function calls, but on the swift side I get no output to console and the text doesn't render. I would really appreciate if you point my mistakes in this, since I get very rarely involved in iOS development.)
ContentView.swift
import Foundation
import SwiftUI
var objectivec_class = Objectivec_Class()
class Foo : ObservableObject {
#Published var bar = ""
}
#objc
class BridgingClass: NSObject {
#ObservedObject var baz = Foo();
#objc func updateString(_ content: NSMutableString) {
print("This function is called from Objective C")
self.baz.bar += content as String
}
}
struct ContentView: View {
/**
* This part seems fishy to me,
* It would have been better to inject the instance of Foo here in
* BridgingClass but, couldn't figure out how to.
* This is only for showing my intention.
*/
#ObservedObject var baz = Foo();
var body: some View {
Button(action: {
objectivec_class.updateSwiftUi()
})
{
Text(self.baz.bar)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Objective C Bridging Header,
Project-Bridging-Header.h
#import "Objectivec_Class.h"
Objectivec_Class.h
#ifndef Objectivec_Class_h
#define Objectivec_Class_h
#import <Foundation/Foundation.h>
#import "Project-Swift.h"
#interface Objectivec_Class : NSObject
#property (strong, nonatomic) NSMutableString* stringWhichWillBeRendered;
#property BridgingClass *bridgingClass;
- (id) init;
- (void) updateSwiftUi;
#end
#endif /* Objectivec_Class_h */
Objectivec_Class.m
#import <Foundation/Foundation.h>
#import "Project-Swift.h"
#import "Objectivec_Class.h"
#implementation Objectivec_Class
- (id)init{
if( self = [super init] ){
_stringWhichWillBeRendered = [NSMutableString stringWithString:#""];
BridgingClass *bridgingClass = [BridgingClass new];
}
return self;
}
- (void) updateSwiftUi {
NSString *thisWillBeRendered = #"Render this string.";
[_stringWhichWillBeRendered appendString:thisWillBeRendered];
[[self bridgingClass] updateString:_stringWhichWillBeRendered];
}
#end

Try the following
#objc
class BridgingClass: NSObject {
var baz = Foo() // here !!
...
and
struct ContentView: View {
#ObservedObject var baz = objectivec_class.bridgingClass.baz // << this !!
var body: some View {
Button(action: {
objectivec_class.updateSwiftUi()
})
{
Text(self.baz.bar)
}
}
}
Objectivec_Class.m
#implementation Objectivec_Class
- (id)init{
if( self = [super init] ){
_stringWhichWillBeRendered = [NSMutableString stringWithString:#""];
self.bridgingClass = [BridgingClass new]; // here !!
...

I would like to answer my own question because I would also like to share how I have achieved this with the help of Asperi.
As a side subject, I had to switch back to the legacy build system because of the cyclic dependency errors I was getting from Xcode. This also implies me that there should be a better way to do all this :)
With that said and with the assumption that you did the prerequisites of bridging between Swift <-> ObjC both ways,
ContentView.swift
import Foundation
import SwiftUI
var objectivec_class = Objectivec_Class()
class Foo : ObservableObject {
#Published var bar = ""
}
#objc
class BridgingClass: NSObject {
#ObservedObject var sharedObj = Foo()
#objc func updateString(_ content: NSMutableString) {
print("This function is called from Objective C (update String)")
sharedObj.bar += content as String
}
}
struct ContentView: View {
#State var stringToBeUpdated = ""
var body: some View {
Button(action: {
objectivec_class!.updateSwiftUi()
self.stringToBeUpdated = objectivec_class!.bridgingClass.sharedObj.bar
})
{
Text(self.stringToBeUpdated.isEmpty ? "tap me" : self.stringToBeUpdated)
}
.background(Color.green)
.frame(height: 100)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Objective C Bridging Header,
Project-Bridging-Header.h
#import "Objectivec_Class.h"
Objectivec_Class.h
#ifndef Objectivec_Class_h
#define Objectivec_Class_h
#import <Foundation/Foundation.h>
// #import "Project-Swift.h"
/** Forward declaring the class and not including the "Project-Swift.h" file
in this header is important if you are using Xcode's legacy build system */
#class BridgingClass;
#interface Objectivec_Class : NSObject
#property (strong, nonatomic) NSMutableString* stringWhichWillBeRendered;
#property BridgingClass *bridgingClass;
- (id) init;
- (void) updateSwiftUi;
#end
#endif /* Objectivec_Class_h */
Objectivec_Class.m
#import <Foundation/Foundation.h>
#import "Project-Swift.h"
#import "Objectivec_Class.h"
#implementation Objectivec_Class
- (id)init{
if( self = [super init] ){
_stringWhichWillBeRendered = [NSMutableString stringWithString:#""];
self.bridgingClass = [BridgingClass new];
}
return self;
}
- (void) updateSwiftUi {
// Probably you did something there to update the string.
NSString *thisWillBeRendered = #"New information appended to string";
[_stringWhichWillBeRendered appendString:thisWillBeRendered];
[[self bridgingClass] updateString:_stringWhichWillBeRendered];
}
#end
Any comments are welcome, newbie here ;)

You can make use of Key-Value Observing (KVO), and have your observable object register as observer for the property you want to monitor:
class Foo : ObservableObject {
// bind the text to this
#Published var bar = ""
// no-one needs to know we delegate the work
private let worker = Objectivec_Class()
init() {
worker.observe(\.stringWhichWillBeRendered, options: [.new]) { [weak self] obj, change in
// the forced unwrap is safe here, due to the `options` parameter
self?.bar = change.newValue!
}
}
// call this from the button action
func update() {
worker.updateSwiftUi()
}
}
KVO will make sure the observation handler will be called every time the monitored property changes. And in turn, the handler will keep the published value in sync with the Objective-C one, which means you can bind your UI elements to bar instead of knowing/caring about the internals of the (View)Model.
No need for an extra bridging (boilerplate) class, no need to change the Objective-C class just to accommodate the SwiftUI design.
You can apply this technique even to classes that you don't have control over, and thus cannot bed changed, e.g. ones from 3rd party libraries (just make sure the properties you want to monitor are KVO-compliant).

Related

Accessing Swift Protocol in Objective C and then back on Swift

I'm a new comer into Objective-C and swift and I need to make changes to an existing code base.
I have added a protocol defined in Swift
File AController.swift
#objc
protocol MyDelegate: NSObjectProtocol {
func saveTapped()
func discardTapped()
}
#objc
class AController: UIViewController, BcAiDelegate {
...
}
The protocol is used as a member is BController.h
and the code compiles and builds w/o issues
// imports are placed here
// #class declarations are here
#protocol MyDelegate;
...
#property (nonatomic) MyDelegate* myDelegate;
#property (nonatomic) BOOL myFlag;
And then I try to access this field in CController.swift
#objcMembers class CController: UIViewController {
...
func doSomething() {
let bController = BController()
// When I try to access `myDelegate` it isn't recognized as a member of the controller
// I get an error message that says - Value of type 'BController' has no member 'myDelegate'
bController.myDelegate = nil
bController.myFlag = true // This lines works fine
}
...
}
If I look at the swift 5 interface generated from BController.h I can see the following definitions:
...
open var myDelegate: UnsafeMutablePointer<Int32>!
open var myFlag: Int33
...
I tried created new initializers in BController.h, one that receives a Bool and the other that receives MyDelegate - and the behavior is the same, the one with Bool is accessible in CController.swift and the other isn't
Obviously I'm missing some part - but what?

"No visible #interface for 'MySwiftFile' declares the selector '****'" in iOS framework

I've been having trouble with using Swift in an ObjC framework in iOS. My framework has Objective-C code, which I want to call Swift code from.
I think I have created the bridging properly, I'll show below what I've done.
MySwiftFile.swift :
open class MySwiftFile: NSObject {
var varDummy : RandomType? = nil
open func setupDummy(param1 : RandomType1) {
varDummy = RandomType(p: param1)
}
}
MyObjCFile.m :
#class MySwiftFile;
#import "MyFramework/MyFramework-Swift.h"
#interface A : NSObject<...>
#property(atomic) MySwiftFile *mySwiftFile;
.....
#end
#implementation Aclass
......
#end
#interface B ()
....
#property(readonly, nonatomic) A *refA;
#end
#implementation B
....
- (void)methodCalledSomewhere:(RandomType1 *)type {
....
refA.mySwiftFile = [[MySwiftFile alloc] init];
[refA.mySwiftFile setupDummy: type]; <====THIS LINE PRODUCES THE ERROR
}
....
To sum it up, I want to init the property and call a function of a Swift object from ObjC code. Xcode seems to recognize MySwiftFile as a valid type, then how come it does not allow me to call the "setupDummy" method?
The errors are 2:
No visible #interface for 'MySwiftFile' declares the selector 'setupDummy:'
No visible #interface for 'MySwiftFile' declares the selector 'setupDummy'
First problem is that you forget to expose it to the Objective-C. Either add #objcMembers to expose everything ...
#objcMembers
open class MySwiftFile: NSObject {
...
}
... or just add #objc to your setupDummy function ...
#objc
open func setupDummy(param1: String) {
...
}
Second problem is about how the function name is translated to Objective-C ...
#objc func setupDummy(param1 : RandomType1) -> setupDummyWithParam1:
#objc func setupDummy(_ param1: RandomType1) -> setupDummy:
#objc(setupDummy:) func setupDummy(param1: String) -> setupDummy:
... which means that adding just #objc wont work. You have to either change the Swift function signature or use setupDummyWithParam1: in your Objective-C or keep the Swift function signature & use #objc(<name>) to change the Objective-C selector.

Swift ViewModel properties can't access in ObjC

In Swift project contains,
CartViewModel.swift
#objc public class CartViewModel: NSObject {
let apiService: APIService
var alertMessage: String? {
didSet {
self.showAlertClosure?()
}
}
var isShow: Bool = false {
didSet {
self.updateStatus?()
}
}
#objc public var dataCount: Int {
return models.count
}
}
ListViewController.h
NS_ASSUME_NONNULL_BEGIN
#class CartViewModel;
#interface ListViewController : UIViewController
#property (nonatomic, strong) CartViewModel *viewModel;
#end
NS_ASSUME_NONNULL_END
ListViewController.m
NSLog(#"%#", self.viewModel.dataCount);
// Accessing any property gives this error
Property 'dataCount' cannot be found in forward class object 'CartViewModel'
If you want to be able to access properties of a class inside the implementation file of an Objective-C class, you need to import the header of the class. Simply forward declaring the class by doing #class YourClass only makes the type itself visible, but it doesn't expose its properties/methods.
Since Swift files don't have headers, you need to import the Swift header of your module.
So in your ListViewController.m do
#import <YourModule/YourModule-Swift.h>
In your Objective-C implementation you need to import the Xcode-generated header file for Swift. Add this to your imports replacing ProductModuleName with your target's name:
#import "ProductModuleName-Swift.h"
Just in case anyone else comes here with this problem; used #class and imported the Swift header but the problem is not solved yet... Double check you have made your Swift class public and not internal

iOS Framework - Use of undeclared identifier

I am currently trying to create my own custom iOS framework using Swift 2.3. I have a test iOS app, written in Obj-C, and it embeds + links to the custom framework.
The framework has multiple classes, two of them resembling the below:
public class Constants: NSObject
{
private static let sharedInstance = Constants()
override init() { }
public static let CONST_VALUE1 = "somevalue1"
public static let CONST_VALUE2 = "somevalue2"
}
and
public class RandomUtils: NSObject
{
private static let sharedInstance = RandomUtils()
override init() { }
public static func randomFunction(someValue: String) -> String?
{
return someValue
}
}
The RandomUtils class has no problems being seen and used in the test app. The Constants class however, cannot seem to be found, despite it being referenced to in the SDK-Swift.h header:
SWIFT_CLASS("_TtC6sdk9Constants")
#interface Constants : NSObject
+ (NSString * _Nonnull)CONST_VALUE1;
+ (NSString * _Nonnull)CONST_VALUE2;
#end
SWIFT_CLASS("_TtC6sdk16RandomUtils")
#interface RandomUtils : NSObject
+ (NSString * _Nonnull)randomFunction:(NSString * _Nonnull)someValue;
#end
In my test app, I am importing the framework umbrella header file in my ViewController's header file as such:
#import <UIKit/UIKit.h>
#import <SDK/SDK-Swift.h>
#interface TestVC : UIViewController
#property (weak, nonatomic) IBOutlet UITextField *txtValue1;
#end
Attempts to use the Constants class
NSLog(#"%#", [Constants CONST_VALUE1]);
result in this error message
Use of undeclared identifier 'Constants'
Does anyone have any idea what I could be doing wrong?
After some trial and error, I resolved the issue by placing the .framework directly into the test app folder.

Objective-C: C struct in #property with custom deallocation function

I have a C struct with custom allocation/deallocation functions because the struct has a dynamically-allocated nested array:
struct Cell {
int data, moreData;
};
struct Grid {
int nrows, ncols;
struct Cell* array;
};
struct Grid* AllocGrid (int nrows, int ncols) {
struct Grid* ptr = (struct Grid*) malloc (...);
// ...
ptr->array = (struct Cell*) malloc (...);
return ptr;
}
void FreeGrid (struct Grid* ptr) {
free (ptr->array);
free (ptr);
}
I want to use this struct in the UIViewController of my Objective-C app. The grid's lifespan should be the same as the controller's one.
If it were a C++ object, I would call AllocGrid() in the constructor and match it with a call to FreeGrid() in the destructor. So I tried to put the allocation in the init message and the deallocation in dealloc:
#implementation ViewController
{
struct Grid* theGrid;
}
- (id)init {
self = [super init];
if (self) {
NSLog(#"init()");
theGrid = AllocGrid(10,10);
}
return self;
}
- (void)dealloc {
NSLog(#"dealloc()");
DeallocGrid(theGrid);
theGrid = NULL;
}
#end
But the allocation is never executed and I cannot see the "dealloc" log message when running the app in the iOS simulator. I guess I could do the allocation in viewDidLoad but I feel it's not the right thing to do. Hence my question:
Question: How can I wrap the C struct in a #property and force it to use my custom AllocGrid() and DeallocGrid() functions?
Or: Is there an equivalent of a scoped_ptr in Objective-C? Or should I roll out my own?
I think putting the allocation in the viewDidLoad() is correct. In fact, there is a discussion regarding to why init() is not being called in ViewController, iPhone UIViewController init method not being called. But, it depends on your context, if you want to initialize your structure "before" the view appear, you should put your initialization in viewWillAppear. There's another interesting thread talking about the invoking order in ViewController, Order of UIViewController initialization and loading. Finally, I want to point out Objective-C is an extension of C, so the "basic" allocation/free behavior should be the same.

Resources