Why Learn SwiftUI as a Flutter Developer?

Bridge the gap

October 29, 2024
October 29, 2024
updated on
October 29, 2024
By 
Guest Contributor

Since the birth of Very Good Ventures (VGV), we have helped our clients build beautiful and scalable Flutter apps. The advantages of Flutter are obvious, but in particular, we’ve seen it help our clients ship faster and more efficiently because you can target many platforms with the same code base. We truly believe that anytime you’re building a mobile or desktop app, you should seriously consider Flutter over native development.

Unfortunately, there are many edge cases that Flutter doesn’t cover. Over the 10 years, Apple has continued to push iOS, iPadOS, and macOS forward by removing functionality from core app and adding it elsewhere so you can engage with it without ever launching the app. For example:

  • 2015 - watchOS: Allows you to interact with your app on a separate device
  • 2016 - watchOS complications: Allows you to see data from watchOS apps without launching the app.
  • 2018 - Siri Shortcuts: The ability to take actions in your app through automated “Shortcuts” instead of launching the app. They can also be triggered by voice with Siri.
  • 2020 - App Clips: Small portions of an app that can be used without installing an entire app.
  • 2020 - Home screen widgets: Small views on the home screen that displays data from the app without launching it. Brought to the iPad in 2021, to macOS in 2023, and made interactive in 2023 so you can take actions directly from the widget.
  • 2022 - Live Activities: A short-lived widget that lives in your notification center during a live event, such as a sports match or food delivery.
  • 2022 - Dynamic Island: Another home for live activities on top of the front-facing camera. It can have many views based on context.
  • 2022 - Lock screen widgets: Like home screen widgets, but smaller and live on the lock screen.
  • 2023 - Standby mode: A home for widgets and live activities while your iPhone is charging.
  • 2024 - Controls: Simple widgets that can appear in the control center, lock screen, and can be triggered by the action button.
  • 2024 - Apple Intelligence: An AI model that allows you to execute actions in your apps without launching them just by voice.

And this isn’t even to mention older features like Now Playing, interactive notifications, SharePlay, iMessage stickers, Apple CarPlay, and more. More and more with each release, Apple users expect the apps they love to be deeply integrated into their operating systems and support all these incredible features. That leaves a Flutter developer working on an iOS app with a few options.

For a few of these use cases, the Flutter community has developed rich plugins that allow you to integrate this functionality right into your Flutter app. But for many of them, especially ones with UI components, you’ll have to write native code. In modern native Apple development, that means Swift. Luckily for us, Swift is a wonderful language that is similar to Dart - they are both object oriented, fully managed memory, null aware, strongly typed, and have lots of convenient syntax for getting things done. Swift is also a fully open source language led by a steering committee at https://swift.org. Furthermore, all these additional targets support SwiftUI, Apple’s reactive UI framework written in Swift. When comparing the code to Flutter, you’ll be surprised how similar and intuitive they both are.

Comparing SwiftUI and Flutter

If you’re a Flutter developer, you’ll know the basic building block of and UI element is a Widget. The process of building an app is composing Widgets within other Widgets, such as Columns and Rows. These Widgets can also contain state, and can re-render when the state is changed. In Swift UI, the equivalent structure is a View. Views act a lot like Widgets - they can contain state and re-render in response to the state changing, they can be composed together, and there are dozens of built-in Views for building your Views.

class MyWidget extends StatelessWidget {
	
	@override
	Widget build(BuildContext context) {
		return Text('Hello World');
	}
}

struct MyView: View {
	var body: some View {
		Text("Hello World")
	}
}

You might notice that the syntax of Swift has a few weird differences from Dart. First, notice we’re using a struct instead of a class. Swift has classes, too, so what’s the difference? In Swift, an object created from a class is bound to a place in memory with a pointer, just like most object oriented languages. If it is assigned to another variable, it is copied by reference. By contrast, a struct is like a combination of primitive values (like Int, for example). When structs are reassigned, they are copied by value; in other words, structs copy all of their values they contain to an entirely new place in memory. You’ll see both classes and structs used in Swift code for different purposes, but for Views, we use struct. In this case, it’s a struct called MyView that extends View.

You might also notice the var body: some View and be confused. In Swift, var is usually used to declare mutable variables (while let is used to declare immutable variables, like final in Dart). However, since this opens into a curly brace, we know this is a getter function. In Swift, type declarations are made after the variable name (just like Typescript), so declaring a variable that contains a string would look like var myVariable: String. The :some View just lets the compiler know that this function should return something that extends View. Finally, notice the lack of return keyword in the body of the getter. In Swift, when there is only one expression in a code block, it assumes you want to return and does it automatically. It is perfectly valid to add return there, but it is mostly omitted since it is optional.

Put all together, if we converted this example super closely into Dart, it might look something like.

/// Converting SwiftUI "Hello World" example 1-for-1 to Dart.
/// Not valid Flutter code!
class MyView extends View {
	View get body {
		return Text('Hello world);
	}
}

State Management

For state management, Flutter uses the StatefulWidget for widgets to keep track of properties and re-render when those properties change. SwiftUI has a very similar mechanism by using the @State property decorator. As you can see, there's no separate View component that needs to be labeled stateful as there is in Flutter. Simply modifying a state property is enough to tell the view to rebuild. So where in Flutter you may need to call SetState to render the screen and display the updated value, there are no additional calls to make in SwiftUI.

class MyWidget extends StatefulWidget { 
	// ...
}

class MyWidgetState extends State {
	int _count = 0;

	@override
	Widget build(BuildContext context) {
	  return Column(
	    children: [
		    Text('You have clicked the button $_count times'),
		    ElevatedButton(
			    onPressed: () {
				    setState(() {
					    _count += 1;
				    });
			    },
			    child: const Text('Increment'),
		    ),
	    ]
    )
	}
}

struct MyView: View {
	@State private var count = 0
	
	var body: some View {
		VStack {
			Text("You have clicked the button \\(count) times")
			Button("Increment") {
				count += 1
			}
		}	
	}
}

In these examples, you can also see the difference in string interpolation, as well as the VStack view, which is equivalent to the Column widget in Flutter. In fact, there are many Widgets in Flutter that have counterparts in SwiftUI, such as

You might also notice our VStack doesn’t contain a list of Views, or even any parameters. Heck, it doesn’t look like a constructor is being called at all! This is mostly just syntactic sugar on the part of Swift. VStack takes a function (called content) that returns the content that it should stack vertically, this is called a ViewBuilder. Since it is the only argument in the VStack constructor that accepts a function, it can be split out without any labeling and Swift assumes you want to pass in the content parameter (you can see the same thing with Button as we’re passing in the callback action without any labeling there too). And, since there are no parameters explicitly passed into VStack, you don’t need the parentheses to call the constructor (even though it is being called). If you want to make it more explicit, you totally can.

struct MyView: View {
	@State private var count
	
	var body: some View {
		VStack(
		content: {
				Text("You have clicked the button \\(count) times"),
				Button("Increment") {
					count += 1
				}
			}
		)
	}
}

When dealing with Views that can be passed data as well as update data, such as a TextField, SwiftUI uses an object called a Binding. You can think of a Binding as a getter and setter combined into one object. They can easily be created from using @State by just prefixing the variable with $. For example:

struct MyView: View {
	@State private var textValue = ""
	
	var body: some View {
		TextField("Example text field", text: $textValue),
	}
}

This means that any time the user types in the TextField, the variable is updated and anything else using that variable will be rebuilt. Additionally, if any other widget updates that variable, it will be updated inside the TextField.

When we need to pass objects down the Widget tree in Flutter, we use the BuildContext . Using InheretedWidget, or packages like Provider, we can access objects like so:

@override
Widget build(BuildContext context) {
	final theme = Theme.of(context);
	final messagesRepository = context.read();
	// ect...
}

SwiftUI has a very similar mechanism called @Environment. It allows you to send objects into the view hierarchy.

struct MyRootView: View {
	var body: some View {
		MyDetailedView()
			.environment(MessagesRepository())
	}
}

struct MyDetailedView: View {
	@Environment(MessagesRepository.self) private var messagesRepository 
	
	var body: some View {
		Text("detail view")
	}
}

This is also used for many build-in items, such as detecting if the app is in light mode or dark mode

struct MyView: View {
    @Environment(\\.colorScheme) var colorScheme

    var body: some View {
        Text(colorScheme == .dark ? "In dark mode" : "In light mode")
    }
}

Modifiers

Finally, there is one rather big difference between Flutter and SwiftUI, and that is how Widgets/Views are modified. In Flutter, Widgets are generally customized in their constructors or wrapped with helper Widgets to build a more dynamic UI. For example, if you wanted to create some blue text with some padding around it, you would do something like this.

class MyWidget extends StatelessWidget {
	
	@override
	Widget build(BuildContext context) {
		return Padding(
			padding: EdgeInsets.all(8.0),
			child: Text(
				'Hello World',
				style: TextStyle(
					color: Colors.blue,
				),
			),
		);
	}
}

In SwiftUI, it looks just a little different.

struct MyView: View {
	var body: some View {
		Text("Hello World")
			.foregroundColor(.blue)
			.padding(8.0)
	}
}

These functions dangling off the Text View are called modifiers, and they are the primary way you customize Views in SwiftUI. You may have noticed before that we used the .environment in our RootView to provide an object in the environment. This is also just another modifier.

To Wrap it Up

Now that we’ve discussed why you might want to learn SwiftUI as a Flutter developer and what the experience could look like, we hope you’ll take away the idea that Flutter and SwiftUI aren’t so different after all. While picking up a new framework can seem daunting, getting started with SwiftUI is a lot easier than it seems, and with just a little bit of SwiftUI added to your existing Flutter app, you can greatly improve the user experience!

Explore this topic further by watching our session delivered at Fluttercon USA 2024!

More Stories