SwiftUI vs Flutter

By Gabriel Cho
Sun Jan 05 2020
In the past distinction between games and apps were clear. Typical games require high frame rates, physics, mesh animations, audio, etc, while apps are composed of a few "views" with navigation buttons and lists filled with heavy texts and/or images. However the line between them are becoming more blurrier as more and more apps are packing multimedia rich contents in them. Take my latest app as an example below:
You can try it from Google Play or Apple App Store.
It is filled with sprite and transition animations, sound, and some texts. It is clearly not a typical game, but not a traditional app either. My go-to framework would have been either Unity3D or HTML5 + PhoneGap, but this time, I attempted it on latest frameworks that everyone has been fussing about: SwiftUI and Flutter. I had no experience in either frameworks prior to this project, so I can safely claim that I am writing this blog with no bias towards either.

Language

SwiftUI uses the programming language Swift of course. Swift was easy to learn and has some pretty neat features. Ability to extend existing types was very delightful. Swift developers boast how powerful generics are, but C++ had template support decades ago. I like that it adopted Objective C's named parameters. Ability to add methods to enums was completely new to me, and was skeptical at first, but after using them, I cannot live without them.
Flutter uses a programming language called Dart. Seasoned JavaScript developers should have no trouble picking it up. It also supports named parameters, but it went further - the order of named parameters does not matter. This is pretty handy. However it does not support type/class extensions - the global functions will have to suffice. A big turn-off for me is the lack of function overloading. Jesus Google! Also, enums cannot be defined inside of a class and is not as elaborate as Swift.
Both languages are easy to learn, but Swift gets my vote here. After using it for over a month, I admit that I love Swift. Dart could easily be replaced by any other language and I will not miss it.

Syntax/Structure

I have to disclose that I am not a fan of iOS storyboard. Actually, I hate all "visual" editing - Android view editor and Unity3D scene editor. They are great for quick prototypes, but when building production ready games and/or apps that need to handle all kinds of device's shapes and sizes, they get in my way more than helping me. Neither SwiftUI nor Flutter uses visual editor. I create views/widgets when I need and place them where I want. There is no drag-and-drop and IDE silently creating bindings in the background. I do not have to fight with layout engine that always gives unexpected results (spoiler alerts: Flutter does have some weird and frustrating hehaviours which are explained later). Finally, I feel like I am in the driver's seat!
In honest, Flutter dropped the ball when it comes to syntax. Let’s take a look at a simple example:

Add a background image:


struct MyView : View {
    var body: some View {
        Image("background")
    }
}

class MyView extends StatelessWidget {
    @override Widget build(BuildContext context) {
        return(Image(image:AssetImage("assets/background.jpg")));
    }
}

Fill the device screen with this background image:


Image("background")
    .resizable().scaledToFill()
    .frame(height:DEVICE_HEIGHT)

return(Container(
    height:DEVICE_HEIGHT,
    decoration:BoxDecoration(
        image:DecorationImage(
            image:AssetImage("assets/background.jpg"), 
            fit:BoxFit.cover
        )
    )
));

Now add a character image:


ZStack {
    Image("character")
}.background(
    Image("background")
        .resizable().scaledToFill()
        .frame(height:DEVICE_HEIGHT)
)        

return(Container(
    width:DEVICE_WIDTH,height:DEVICE_HEIGHT,
    decoration:BoxDecoration(
        image:DecorationImage(
            image:AssetImage("assets/background.jpg"), 
            fit:BoxFit.cover
        )
    ),
    child:Image(image:AssetImage("assets/character.png"))
));            

Make the character height 50% of the device screen, and place it at the bottom of the screen:


ZStack {
    Image("character")
        .resizable().scaledToFit()
        .frame(height:DEVICE_HEIGHT/2)
        .offset(y:DEVICE_HEIGHT/4)
        
}.background(
    Image("background")
        .resizable().scaledToFill()
        .frame(height:DEVICE_HEIGHT)
)

return(Container(
    alignment:Alignment.center,
    height: DEVICE_HEIGHT,
    decoration:BoxDecoration(
        image:DecorationImage(
            image:AssetImage("assets/background.jpg"), 
            fit:BoxFit.cover
        )
    ),
    child:Stack(
        alignment:Alignment.center,
        child:Positioned(
            bottom:0,
            child:Container(
                height: DEVICE_HEIGHT/2,
                child:Image(image:AssetImage("assets/card0.png"))
            )
        )
    )
));
In Flutter, to position an element, not only does positioned element need to be enclosed in a Positioned widget, but this Positioned widget also needs to be a child of Stack container. For now, I can forgive this extra 2-level deep tree.

Now rotate 45 degrees and scale by 2:


ZStack {
    Image("character")
        .resizable().scaledToFit()
        .frame(height:DEVICE_HEIGHT/2)
        .scaleEffect(2.0).rotationEffect(Angle.degrees(45))
        .offset(y:DEVICE_HEIGHT/4)
}.background(
    Image("background")
        .resizable().scaledToFill()
        .frame(height:DEVICE_HEIGHT)
)            

return(Container(
    alignment:Alignment.center,
    height: DEVICE_HEIGHT,
    decoration:BoxDecoration(
        image:DecorationImage(
            image:AssetImage("assets/background.jpg"), 
            fit:BoxFit.cover
        )
    ),
    child:Stack(
        alignment:Alignment.center,
        child:Positioned(
            bottom:0,
            child:Container(
                height: DEVICE_HEIGHT/2,
                child:
                    Transform.scale(
                        scale:2, origin:Offset(0,0),
                        child:
                            Transform.rotate(
                                angle:45*pi/180,
                                child:Image(image:AssetImage("assets/card0.png"))
                            )
                    )
            )
        )
    )
));
I can go on and on: add opacity, gesture, border, padding, and so on. I spent more time counting parenthesis in Flutter than writing code! To simplify this mess, I will have to take nested children out and make them into their own functions or properties.
Unless I am buliding a simple app that all elements are laid out horizontally and/or vertically, I have to use heavy transformations. As shown above, translation, rotation and scale are simple method calls in SwiftUI. In Flutter, I either have to nest them in three or more different nested widgets or use transformation matrix as follows:

Matrix4 m = Matrix4.identity();
m.translate(IMAGE_WIDTH/2, IMAGE_HEIGHT/2 + DEVICE_HEIGHT/4, 0);
m.rotateZ(45*pi/180);
m.scale(2.0, 2.0, 1.0);
m.translate(-IMAGE_WIDTH/2, -IMAGE_HEIGHT/2, 0);

return(Container(
    alignment:Alignment.center,
    height: DEVICE_HEIGHT,
    decoration:BoxDecoration(
        image:DecorationImage(
            image:AssetImage("assets/background.jpg"), 
            fit:BoxFit.cover
        )
    ),
    child:Container(
        alignment:Alignment.center,
        width:IMAGE_WIDTH,height:IMAGE_HEIGHT,
        transform:m,
        child:Image(image:AssetImage("assets/card0.png"))
    )
));
I prefer this over half a dozen nested Widgets. Even if I do not need scale or rotation, using translation is simpler than Stack > Positioned > Container tree. However after spending considerable time converting SwiftUI views to Flutter Widgets, I found out that the element's hitbox does not transform with the transform matrix!!! In another words, if I want to detect a tap event, I cannot do so using transform matrix. Code below fails to capture a tap event if the child container is transformed.

...
return(GestureDetector(
    onTap:() { print("clicked"); },
    child:Container(transform:...)
))
Also Flutter does NOT support z-index. In my app, I have a list of 22 cards, and when a user picks one card, this card needs to appear on top of other cards. In SwiftUI, it's as simple as setting z-index to 1 for the selected item. In Flutter, I have to shuffle the list of cards so that the selected card is at the end of the list while keeping its "key" (or id) in original position. Below is a snippet of code from my app that "sorts" list of cards:

// freaking flutter... no z-index support
List<int> ids = List.generate(count, (i) => i);
ids.sort((left,right) {
    if (TarotStates.i.selected.contains(left) && TarotStates.i.selected.contains(right)) {
        return( (TarotStates.i.selected.indexOf(right) - TarotStates.i.selected.indexOf(left)) );
    }
    else if (TarotStates.i.selected.contains(left)) return(1);
    else if (TarotStates.i.selected.contains(right)) return(-1);
    else return(0);
});

List<Widget> children = List.generate(count, (i) {
    int index = ids[i];
    Matrix4 m = Matrix4.identity();
    m.translate(offsetX(index), offsetY(index), 0);
    m.scale(scale(index));
    return(Transform(
        alignment:Alignment(0.0, 0.0),
        transform:m,
        child:Container(
        key:Key(index.toString()),
        alignment: Alignment.center,
        child: CardSpread(deck: deck, index: index, onClick: onClick)
    )));
});

... // followed by again nested GestureDetector, Stack, Positioned, etc
Compare this with SwiftUI using zIndex (the difference is night and day!):

ZStack {
    ForEach(0..<count, id:\.self) { i in
        CardSpread(index: i, visible:i < self.count, deck:self.deck, onClick:self.onClick)
        .scaleEffect(self.scale(i)).offset(x:self.offsetX(i),y:self.offsetY(i))
        .zIndex(TarotStates.i.selected.contains(i) ? 1 : 0)
    }
}
So far, my frustration with Flutter is just the tip of the iceberg. Let's take a look at the following Flutter examples:

return(Container(
    width:200,height:200,
    decoration:BoxDecoration(color:Colors.blue)
    child:Container(
        width:100,height:100,
        decoration:BoxDecoration(color:Colors.red)
    )
));          
and:

return(Container(
    width:200,height:200,
    decoration:BoxDecoration(color:Colors.blue)
    child:Container(
        width:400,height:400,
        decoration:BoxDecoration(color:Colors.red)
    )
));          
Why is the inner container in the first example not 100x100, but 200x200, or the container in the second example not 400x400, but 200x200? If it is not going to honor them, it should not call them width/height, but "preferredWidth/preferredHeight". I spent hours debugging why image sizes were all wrong.
Also throughout my examples, I used DEVICE_HEIGHT. In SwiftUI, getting device height is pretty easy, though, I wish it was some global function I could query. However in Flutter, I have to use a hack-ish sleep/retry, because height keeps changing while Toolbar / ActionBar / StatusBar is being dismissed on startup.

var body : some View {
    GeometryReader() { geo in 
        Color.black.frame(width:geo.size.width, height:geo.size.height)
    }
}

load(BuildContext context) async {
    // jesus Flutter... need to wait until toolbar disappears
    await Future.delayed(Duration(milliseconds:100));
    MediaQueryData data = MediaQuery.of(context);
    width = data.size.width;
    height = data.size.height;
    safeTop = data.viewPadding.top;
    safeLeft = data.viewPadding.left;
    safeRight = data.viewPadding.right;
    safeBottom = data.viewPadding.bottom;
}            
I admit that I rant a lot, but working in Flutter, I have lost more hair in the last few weeks than an entire year.

Sprite Animations

Unfortunately neither frameworks provide built-in sprite animations nor atlas support. I had to crop images from an atlas manually in both frameworks. It's year 2019, and any new frameworks should support simple 2D mesh animation, or at the least provide atlas support.

Audio

In SwiftUI, I can still use AVFoundation.AVAudioPlayer, so playing audio is pretty straight forward. However, I had to rely on 3rd party plugin for Audio support in Flutter... I don't know about other developers, but I hate using 3rd party plugins. I already had to rewrite portion of code after updating the plugin version to the latest, because the author decided to change syntax. This happens too often in 3rd party plugins in my experience. And at the time of writing, this plugin fails on hot restart ("hot restart" is described later).

Text/Paragraphs

SwiftUI supports auto-resizing multi-line paragraph out of the box. Again I had to rely on 3rd party plugin in Flutter.

Transition animation

Undoubtedly, SwiftUI developers thought of transition animations from the get-go. In SwiftUI, adding transition is literally a few of lines of code. Following code will fade-in the background image.

struct SomeView : View {
    @State var opacity:Double = 0;
    var body: some View {
        Image("background")
            .opacity(opacity)
            .animation(.easeOut(duration:0.5))
            .onAppear(){self.opacity=1}
    }
}
In Flutter, I have to animate everything myself. In another words, I need to convert the widget to "Stateful", create an animation controller, a tweener, and refresh the widget whenever the tween value changes. So to accomplish the same in Flutter:

class SomeView extends StatefulWidget {
    @override _SomeViewState createState() => _SomeViewState();
}
class _SomeViewState extends State with SingleTickerProviderStateMixin {
    AnimationController ctrl;
    Animation opacity;
    onUpdate(){
        setState((){});
    }
    @override initState(){
        super.initState();
        ctrl = Animation(duration:Duration(milliseconds:500), vsync:this);
        opacity = Tween(begin:0,end:1).animate(ctrl)..addListener(onUpdate);
        ctrl.forward();
    }
    @override dispose(){
        ctrl.dispose();
        super.dispose();
    }
    @override Widget build(BuildContext context){
        return(Opacity(
            opacity:GUtils.easeOut(opacity.value),
            child:Image(image:AssetImage("assets/background.jpg"))
        ));
    }
}
Thankfully I can make this a reusable widget:

class FadeIn extends StatefulWidget {
    Widget child;
    FadeIn({Key key,this.child}) : super(key:key);
    @override _FadeInState createState() => _FadeInState();
}
class _FadeInState extends State with SingleTickerProviderStateMixin {
    AnimationController ctrl;
    Animation opacity;
    onUpdate(){
        setState((){});
    }
    @override initState(){
        super.initState();
        ctrl = Animation(duration:Duration(milliseconds:500), vsync:this);
        opacity = Tween(begin:0,end:1).animate(ctrl)..addListener(onUpdate);
        ctrl.forward();
    }
    @override dispose(){
        ctrl.dispose();
        super.dispose();
    }
    @override Widget build(BuildContext context){
        return(Opacity(
            opacity:GUtils.easeOut(opacity.value),
            child:widget.child
        ));
    }
}            
Now I can make any widgets children of FadeIn widget to apply fade-in effect:

return(FadeIn(
    child:Image(image:AssetImage("assets/background.jpg"))
));

Complex animations

SwiftUI blows Flutter out of water when it comes to simple animations, however when animation is beyond simple state change from A to B, Flutter starts to shine. As shown earlier, in Flutter I have total control over animations, so what I can do is up to my imagination. I can start, pause, and stop when I want to. I also get constant animation status callback. SwiftUI unfortunately provides no ability to pause or stop animation (I had to hack to accomplish this), nor does it provide any callbacks. To create complex animations, I had to use parametric functions. For instance, if I were to fade in an image followed by scale down:

struct SomeAnimation : GeometryEffect {
    @Binding var opacity:Double
    var width:CGFloat
    var height:CGFloat
    var t:CGFloat                   // t = 0...2
    var animatableData: CGFloat {
        get { t }
        set { t = newValue }
    }
    func effectValue(size: CGSize) -> ProjectionTransform {
        if (t>=1) {
            // scale down
            return ProjectionTransform(GMatrix4().translate(-width/2,-height/2,0).scale(2-t,2-t,1).translate(width/2,height/2,0))
        }
        else {
            // fade-in
            DispatchQueue.main.async {
                self.opacity = Double(self.t)
            }
            return(ProjectionTransform())
        }
    }
}

struct SomeView : View {
    @State var opacity:Double = 0
    @State var t:CGFloat = 0

    var body: some View {
        Image("character")
            .opacity(opacity)
            .modifier(SomeAnimation(opacity:$opacity,width:IMAGE_WIDTH,height:IMAGE_HEIGHT,t:t))
            .animation(.linear(duration:1))
            .onAppear(){self.t = 2}
    }
}
If there were onComplete in SwiftUI, code above could be simplified to:

struct SomeView : View {
    @State var opacity:Double = 0
    @State var scale:CGFloat = 1
    var body:some View {
        Image("character")
            .opacity(opacity)            
            .scaleEffect(scale)
            .onAppear(perform:start)
    }
    func start(){
        withAnimation(.linear(duration:0.5), onComplete:scaleDown) {
            self.opacity = 1
        }
    }
    func scaleDown(){
        withAnimation(.linear(duration:0.5)) {
            self.scale = 0
        }
    }
}

State management

SwiftUI has several state variable supports: @State, @EnvironmentObject, and @ObservedObject. In my app, when a user changes language, all text in all views must be updated. In SwiftUI, it is as simple as dropping @EnvironmentObject variable in any views that need to "watch" state value changes:

class UserSettings : ObsevableObject {
    @Published var lang:String = "en"
}
struct View1 : View {
    @EnvironmentObject var settings:UserSettings
    ...
}
struct View2 : View {
    @EnvironmentObject var settings:UserSettings
    ...
}
When UserSettings.lang is updated anywhere in the app, all views watching this variable will be automatically refreshed. Flutter doesn't have such functionality out of the box, and I had to implement something similar that mimics this by keeping list of States, and firing off a refresh call whenever a property value is updated.

class ObservableObject {
    List<State> _observers = [];                // list of "listeners"
    ObservableObject subscribe(State state) {   // add listener
        _observers.add(state);
        return(this);
    }
    ObservableObject unsubscribe(State state) { // remove listener
        _observers.remove(state);
        return(this);
    }
    void notify(){                              // fire refresh to all listeners
        for(var i=0,size=_observers.length; i<size; i++) _observers[i].setState((){});
    }
}
            
class UserSettings extends ObservableObject {
    Static UserSettings i = UserSettings();
    
    // for every freaking property values, I need to manually fire "refresh" on set
    String _lang = "en";
    String get lang => _lang;
    set lang(String val) => {
        _lan = val;
        notify();
    }
}

class View1State extends State {
    @override initState(){
        super.initState();
        UserSettings.i.subscribe(this);     // add this to listeners
    }
    @override dispose(){
        UserSettings.i.dispose(this);       // remove this from listeners
        super.dispose();
    }
    ...
}
SwiftUI's simple state management does come with critical limitations. As an example, let's try to create a self-contained counter widget that simply counts up every second. This widget lets parent views to change color of the text, but its internal counter should not be exposed:

struct Counter : AnimatableModifier {
    @Binding var count:Int
    var countTo:Double
    var animatableData : Double {
        get { countTo }
        set { countTo = newValue }
    }
    func body(content:Content) -> some View {
        let c = Int(countTo)
        if (c != count) {
            DispatchQueue.main.async { self.count = c }  
        }
        return(content
            // maybe a bug with SwiftUI, but without any modifiers, animator doesn't work >:|
            .padding(EdgeInsets(top:0,leading:0,bottom:0,trailing:0))
        )     
    }
}
struct CounterView : View {
    var color:Color
    @State var count: Int = 0
    @State var countTo: Double = 0
    var body:some View {
        Text(String(count))
            .foregroundColor(color)
            .modifier(Counter(count:$count,countTo:countTo))
            .animation(.linear(duration:1000))
            .onAppear(perform:start)
    }    
    func start(){
        countTo = 1000
    }
}
Now in this silly example, let's say whenever the color is changed, the widget needs to reset the counter and counts up again from 0. I could not find a simple way to accomplish this. How is this relevant? In my app, I have a SpriteAnimator widget that takes imageKey, and plays sprite animations frame by frame. When the imageKey is changed, SpriteAnimator needs to "reset" and play again from the first frame. To accomplish this, I had to use a number of hacks. If SwiftUI had a hook such as Flutter's didUpdateWidget or ReactNative's componentWillReceiveProps, it would have been a simple task.
In my view, SwiftUI's and Flutter's "pure" declarative approach (compared to a hybrid approach of ReactNative) does more harm than good. For instance, I have a password TextInput field, and when a user enters a wrong password, I wish to shake the input field, change the border color to red, focus the input field, and finally select the text. Both SwiftUI and Flutter can easily do the first two with simple state changes, but latter two are impossible in SwiftUI, and extremely complex in Flutter. TextInput having focus, selecting text, ScrollView setting scroll position, and so on cannot be mapped to certain "states", because they are "behaviours" at instance of time. ReactNative provides "createRef" that lets developers access these UI elements and invoke functions such as "focus", and "scrollTo". From purists' view, this may look like breaking ten commendments, but what's the alternatives? I would rather write clean a few lines of code below than using convoluted workaround:

struct MyPassword : View {
    @State var password:String = ""
    @State var error:String = ""
    @State var reference:SomeInputReferenceType
    var body:some View {
        VStack {
            SecureField("Password", $password).createRef($reference)
            Button(action:login) { Text("Login") }
            Text(error).foregroundColor(Color.red)
        }
    }
    func login(){
        if (password != "my secure password") {
            error = "Wrong"
            reference.focus()
        }
    }
}
If this looks like an eye sore, and they would rather me to use UIViewPresentable to wrap UIKit's UITextField, I would say "thank you, but no thank you", and use other frameworks.

Documentation

SwiftUI is relatively new (released in last fall), but that shouldn't be an excuse for lack of documentations from a trillion dollar company. For instance, at the time of writing, AnimatableModifier has one line documentation:
A modifier that can create another modifier with animation.
This is as bad as my documentation! Unlike Apple though, I work alone, and other developers do not have to look at my code. I had to figure everything out by trial and error. Flutter documentation is a lot better with examples.

Polishing

Flutter loads all assets asynchronously. In practice, this is good, because it will not hog main thread. But I have to question if that is an overkill for images from local assets. Anyways, to prevent "flickering", it offers "precacheImage" method that in theory, should let me preload images, but it is such a hassle to do this every time I need to show an image. All views and sub-views that contain images, must be converted to StatefulWidget, have a "ready" state, override didChangeDependencies function and call precacheImage function on all images, wait for them to load, then finally set "ready" state to true! So as all sane developers would do, I pre-load all images on start-up as follows:

class SomeViewState extends State<SomeView> {
    bool ready = false;
    
    @override Widget build(BuildContext context) {
        if (!ready) return(Container(alignment:Alignment.center,child:Text("Loading")));
        else return(Container(
            alignment:Alignment.center,
            decoration:BoxDecoration(
                color:Colors.red,
                image:DecorationImage(image:AssetImage("assets/background.jpg"), fit:BoxFit.cover)),
            ));
    }
    @override void didChangeDependencies() {
        super.didChangeDependencies();
        preload();
    }
    void preload() async {
        List<String> images = (jsonDecode(await DefaultAssetBundle.of(context).loadString("assets/assets.json")) as List<dynamic>).cast<String>();
        //images = ["background.jpg"];
        List<Future> imgs = [];
    
        for(int i=0,size=images.length; i<size; i++) {
            Image img = Image(image:AssetImage("assets/${images[i]}"));
            imgs.add(precacheImage(img.image, context));
        }
    
        Future.wait(imgs).then((e) { 
            setState(() {ready=true;});
        });
    }
}
Well this bloody thing does not work. When I run it, I see a red background briefly. If I pre-load only a few images, then it works. So I am guessing that precacheImage discards cached images when it runs out of memory. Technically this is good, since it is much better than crashing, but it means I have to maintain list of assets that are served in individual views and implement preload logic above. Yuck!

Deployment

Unless an app is a prototype, Firebase is likely a mandatory requirement. I would have chosen good old Google Analytics, if Google did not retire it. I have been using Firebase since it was in beta, and it has been one of the most frustrating SDK's to integrate - specifically in Unity3D. Since Flutter is developed by Google, I thought that this time around integration would be seamless. Boy, was I wrong!! I simply added Firebase library and dropped in google-services.json and the project failed to compile. As usual, I had to juggle with different gradle version numbers, those dreadful support library version numbers, etc to finally compile. But then it failed to build an app bundle, because of the ridiculous dex 64k limit. I basically scoured the internet, tried all suggestions (at this point, I don't know what made it work), and managed to build and run. Then the app of course crashed with "Class not defined ..." After many hours of more fiddling with various configuration files, it finally ran. I cannot wait to add Facebook SDK later because they get along with Firebase so well...
One of Flutter's selling point is platform independence. SwiftUI is only available on iOS (also only on iOS 13 and up). Flutter apps work on both Android and iOS. To my surprise, it ran on iOS, after addressing Firebase crashing on statup (of course). However this cannot simply right a wrong above. Besides, if the platform independence was a deciding factor, there are many other choices that are much more mature.
Unlike Android, bundling and deploying SwiftUI version went without a hiccup.

Testing/Debugging

Another Flutter's selling point is this "hot reload"/"hot restart". Basically when code is updated, instead of recompiling the project, reinstall the apk to a device, and restart from scratch, Flutter can simply push the updated view on the fly. Well yes, it does work to a certain extent. But beyond simple Hello World or some tutorial apps, "hot reload" simply does not work. At least it never works in my project. So I have to click "Stop" and then "Play" button (i.e. cold restart), and this process takes several minutes. Ironically SwiftUI cold restarts all the time and it only takes a few seconds. Go figure...
Xcode is extremely buggy with SwiftUI at the moment. When I make a syntax error (missing comma, colon, quotes, etc), the error message is completely irrelevant and points to wrong line # or even wrong files! While gaining more knowledge in SwiftUI, I re-factored a few times, and whenever I "touched" multiple files at once, I had to spend whole afternoon fixing all those compilation errors by commenting out whole chunk of code and uncommenting one at a time to find syntax errors (reminds me of days with missing } in javascript). Also, SwiftUI's body closure cannot have multiple statements, so I cannot add "print" statements! This is extremely inconvenient because I have to write a function and call it from body closure to simply debug some messages. Well no frameworks are perfect.

Final verdict

SwiftUI gets 3.7/5, while Flutter 2.6/5. There were some challenges but I have to admit developing on SwiftUI was pleasant. On the other hand Flutter experience was a total drag - I almost gave up after I found out that gesture did not work if an element was transformed. Now the answer to the big question, "will I use either framework again?": if the scope is small, I will definitely use SwiftUI again. However I will definitely stay away from Flutter. I would not go as far as predicting that Flutter will be one of hundreds of Google's RIP projects, but at its current state, I simply cannot recommend it. I would stick with HTML5 for now until they cleanup their act or a better framework comes along.