Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Updating SwiftUI view from an Objective C Class

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


like image 521
Ali Somay Avatar asked Jun 05 '20 22:06

Ali Somay


2 Answers

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 !!

...
like image 151
Asperi Avatar answered Oct 13 '22 02:10

Asperi


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 ;)

like image 23
Ali Somay Avatar answered Oct 13 '22 00:10

Ali Somay