· Dhiraj Gupta · Apple  · 8 min read

Transitioning from UIKit to SwiftUI

14 things you need to understand to build SwiftUI Multiplatform iOS and macOS Apps

14 things you need to understand to build SwiftUI Multiplatform iOS and macOS Apps

Are you a seasoned UIKit developer transitioning to SwiftUI for iOS and macOS apps? SwiftUI’s declarative approach revolutionizes how we build user interfaces, offering a unified, efficient workflow for developing multiplatform apps. SwiftUI isn’t just a new framework—it’s a paradigm shift. With support for live previews, multiplatform compatibility, and seamless integration with Combine, it’s the future of app development for Apple platforms.

Let’s go over some essential topics to help you make the leap from UIKit to SwiftUI seamlessly.

1. Introduction to SwiftUI

  • Overview of declarative programming in SwiftUI vs. imperative programming in UIKit.
  • Anatomy of a SwiftUI app: Views, Modifiers, and View Hierarchies.

2. Creating and Composing Views

  • Understanding basic SwiftUI views (Text, Image, Button, etc.).
  • Composing custom views using the View protocol.
  • Leveraging @ViewBuilder for conditional and dynamic view composition.

3. State Management

  • Property wrappers: @State, @Binding, @EnvironmentObject, @ObservedObject, @StateObject.
  • Passing data between views and managing updates.

4. Navigation and Routing

  • Building navigation with NavigationStack and NavigationSplitView.
  • Programmatic navigation and deep linking.

5. Data Handling and Lists

  • Using List, ForEach, and other dynamic container views.
  • Working with Core Data, JSON APIs, or local files for data-driven UIs.

6. Styling and Customization

  • Using modifiers to style views.
  • Customizing fonts, colors, and spacing with system or custom assets.

7. Animations

  • Using implicit and explicit animations with withAnimation.
  • Creating complex animations with TimelineView and Canvas.

8. Gestures

  • Handling gestures such as tap, drag, pinch, and rotate.
  • Composing gestures for custom interactions.

9. Multiplatform Design Considerations

  • Understanding platform-specific features and UI expectations.
  • Using #if os(macOS) and .platform-specificModifier.

10. Integration with UIKit

  • Embedding UIKit views in SwiftUI using UIViewRepresentable.
  • Embedding SwiftUI views in UIKit with UIHostingController.

11. Previewing and Testing

  • Leveraging Xcode previews for live UI testing.
  • Unit testing SwiftUI views.

12. Accessibility

  • Adding accessibility labels, hints, and roles.
  • Testing accessibility using VoiceOver.

13. Performance Optimization

  • Lazy loading with LazyVStack, LazyHStack, and LazyGrid.
  • Profiling with Xcode Instruments.

14. Using Combine with SwiftUI

  • Understanding @Published and how it integrates with SwiftUI.
  • Using Publisher and Subscriber for reactive data flows.

Detailed Explanations

1. Introduction to SwiftUI

SwiftUI is a declarative framework introduced by Apple to simplify UI development across iOS, macOS, watchOS, and tvOS. Unlike UIKit, where you imperatively specify “how” the UI should be built, SwiftUI lets you declare “what” the UI should look like, and it handles the rest.

  • Key Advantages of SwiftUI:

    1. Reduced boilerplate code.
    2. Unified codebase for multiple platforms.
    3. Reactive updates using state-driven programming.
  • View Lifecycle in SwiftUI:
    SwiftUI views are lightweight, value types, and rebuilt frequently. This makes managing the UI state simpler since it’s reactive and automatically re-renders when state changes.

2. Creating and Composing Views

SwiftUI is built around the View protocol, where every piece of UI is a view.

  • Basic Views:
    Examples:

    Text("Hello, SwiftUI!")  
    Image(systemName: "star.fill")  
    Button("Tap Me", action: { print("Tapped!") })  
    
  • Modifiers:
    Views are styled and customized using modifiers, which return new views:

    Text("Stylized Text")
        .font(.title)
        .foregroundColor(.blue)
        .padding()
    
  • Custom Views:
    Create reusable UI components:

    struct CustomView: View {
        var title: String
        
        var body: some View {
            Text(title)
                .font(.headline)
        }
    }
    
  • @ViewBuilder:
    Allows conditional and dynamic content:

    @ViewBuilder
    var dynamicView: some View {
        if Bool.random() {
            Text("True")
        } else {
            Text("False")
        }
    }
    

3. State Management

SwiftUI’s reactive paradigm relies on various property wrappers to manage state.

  • @State:
    Local state within a single view.

    @State private var counter = 0
    
    var body: some View {
        VStack {
            Text("Count: \(counter)")
            Button("Increment") {
                counter += 1
            }
        }
    }
    
  • @Binding:
    Passes a state from parent to child views.

    struct CounterView: View {
        @Binding var counter: Int
    
        var body: some View {
            Button("Increment") {
                counter += 1
            }
        }
    }
    

    In the parent:

    CounterView(counter: $parentCounter)
    
  • @ObservedObject and @StateObject:
    For external models conforming to ObservableObject.

    class CounterModel: ObservableObject {
        @Published var count = 0
    }
    
    struct CounterView: View {
        @ObservedObject var model: CounterModel
    }
    

4. Navigation and Routing

SwiftUI provides flexible navigation solutions that are both declarative and dynamic.

  • NavigationStack
    A modern replacement for NavigationView, supporting stack-based navigation.

    NavigationStack {
        List {
            NavigationLink("Go to Detail", destination: Text("Detail View"))
        }
    }
    
  • NavigationSplitView
    Ideal for apps with a sidebar or master-detail interface, particularly on macOS.

    NavigationSplitView {
        List(["Item 1", "Item 2"], id: \.self) { item in
            NavigationLink(item, destination: Text("Detail: \(item)"))
        }
    } detail: {
        Text("Select an item")
    }
    
  • Programmatic Navigation
    Using State to trigger navigation dynamically:

    @State private var navigate = false
    
    var body: some View {
        NavigationStack {
            Button("Go to Detail") {
                navigate = true
            }
            .navigationDestination(isPresented: $navigate) {
                Text("Detail View")
            }
        }
    }
    

5. Data Handling and Lists

Handling dynamic data is core to SwiftUI, especially when working with collections.

  • List Basics:
    A List is used to display a scrollable column of views.

    let items = ["Apple", "Banana", "Cherry"]
    
    var body: some View {
        List(items, id: \.self) { item in
            Text(item)
        }
    }
    
  • ForEach:
    For building custom layouts with dynamic data.

    let items = ["Apple", "Banana", "Cherry"]
    
    var body: some View {
        VStack {
            ForEach(items, id: \.self) { item in
                Text(item)
            }
        }
    }
    
  • Lazy Stacks and Grids:
    Optimized containers for large datasets.

    LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) {
        ForEach(0..<50, id: \.self) { item in
            Text("Item \(item)")
        }
    }
    
  • Data Persistence:
    Integrating with Core Data or API calls. For example, binding a @FetchRequest with Core Data:

    @FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Item.name, ascending: true)]) 
    private var items: FetchedResults<Item>
    
    var body: some View {
        List(items, id: \.self) { item in
            Text(item.name ?? "Unnamed")
        }
    }
    

6. Styling and Customization

SwiftUI encourages the use of modifiers for styling components, ensuring a clean and reusable design.

  • Text Styling:
    Customize text with modifiers.

    Text("Welcome")
        .font(.largeTitle)
        .foregroundColor(.blue)
        .padding()
    
  • Shapes and Backgrounds:
    Adding rounded corners, shadows, and backgrounds:

    RoundedRectangle(cornerRadius: 10)
        .fill(Color.blue)
        .frame(width: 100, height: 50)
        .shadow(radius: 5)
    
  • Custom Fonts and Colors:
    Use custom fonts and assets:

    Text("Custom Font")
        .font(.custom("YourFontName", size: 18))
        .foregroundColor(Color("CustomColor"))
    
  • Environment Modifiers:
    Apply styles globally:

    VStack {
        Text("Child 1")
        Text("Child 2")
    }
    .foregroundColor(.red) // Applies to all child views
    

7. Animations

SwiftUI simplifies creating animations with declarative syntax.

  • Implicit Animations:
    Automatically animates view changes when state changes.

    @State private var isExpanded = false
    
    var body: some View {
        Rectangle()
            .frame(width: isExpanded ? 200 : 100, height: 100)
            .animation(.easeInOut, value: isExpanded)
            .onTapGesture {
                isExpanded.toggle()
            }
    }
    
  • Explicit Animations:
    Use withAnimation for more control.

    withAnimation(.spring()) {
        isExpanded.toggle()
    }
    
  • Custom Animations:
    Create advanced animations using keyframes or paths:

    @State private var offset = 0.0
    
    var body: some View {
        Circle()
            .offset(x: offset)
            .onAppear {
                withAnimation(.linear(duration: 2).repeatForever(autoreverses: false)) {
                    offset = 300
                }
            }
    }
    

8. Gestures

SwiftUI provides robust gesture handling APIs.

  • Basic Gestures:

    Text("Tap Me")
        .onTapGesture {
            print("Tapped!")
        }
    
  • Composing Gestures:
    Combine gestures for complex interactions:

    let tap = TapGesture()
    let longPress = LongPressGesture(minimumDuration: 1)
    
    var body: some View {
        Text("Tap or Long Press")
            .gesture(tap)
            .simultaneousGesture(longPress)
    }
    

9. Multiplatform Design Considerations

SwiftUI enables developers to build apps for multiple platforms, but each platform has unique design guidelines and capabilities. Here’s how to handle them effectively:

  • Platform-Specific Layouts:
    Use conditional compilation to adapt layouts:

    var body: some View {
        VStack {
            Text("Hello, Multiplatform!")
            #if os(macOS)
            Text("Running on macOS")
            #else
            Text("Running on iOS")
            #endif
        }
    }
    
  • Modifier Variations:
    Adjust modifiers per platform:

    Text("Platform Specific")
        .padding()
        .background(Color.blue)
        .cornerRadius(10)
        .platformSpecificModifier
    
  • Environment Adjustments:
    Use EnvironmentValues to adapt to different device characteristics, such as size classes or safe areas.

    @Environment(\.horizontalSizeClass) var sizeClass
    
    var body: some View {
        if sizeClass == .compact {
            Text("Compact Layout")
        } else {
            Text("Regular Layout")
        }
    }
    

10. Integration with UIKit

SwiftUI is designed to coexist with UIKit, making it easier to migrate incrementally.

  • Embedding UIKit in SwiftUI:
    Use UIViewRepresentable or NSViewRepresentable for custom UIKit/macOS components.

    struct UIKitLabel: UIViewRepresentable {
        func makeUIView(context: Context) -> UILabel {
            let label = UILabel()
            label.text = "UIKit in SwiftUI"
            label.textAlignment = .center
            return label
        }
    
        func updateUIView(_ uiView: UILabel, context: Context) {}
    }
    
  • Embedding SwiftUI in UIKit:
    Use UIHostingController:

    let swiftUIView = UIHostingController(rootView: Text("SwiftUI in UIKit"))
    navigationController.pushViewController(swiftUIView, animated: true)
    

11. Previewing and Testing

SwiftUI’s live previews and testing tools significantly improve development speed and feedback loops.

  • Live Previews in Xcode:
    SwiftUI previews show a live, interactive version of your UI.

    struct MyView_Previews: PreviewProvider {
        static var previews: some View {
            MyView()
                .previewLayout(.sizeThatFits)
                .preferredColorScheme(.dark)
        }
    }
    
  • Dynamic Previews:
    Test different states of your views:

    struct MyView_Previews: PreviewProvider {
        static var previews: some View {
            ForEach(["English", "Spanish"], id: \.self) { locale in
                MyView()
                    .environment(\.locale, .init(identifier: locale))
            }
        }
    }
    
  • Testing Views:
    Use XCTest to verify SwiftUI views. For example:

    func testViewRendersCorrectly() {
        let view = MyView()
        XCTAssertNotNil(view.body) // Ensure the view has a body
    }
    

12. Accessibility

Accessibility is critical for inclusive app design, and SwiftUI simplifies adding it.

  • Adding Accessibility Labels:
    Use .accessibilityLabel, .accessibilityHint, and .accessibilityValue:

    Button("Tap Me") {
        print("Tapped!")
    }
    .accessibilityLabel("Tap Button")
    .accessibilityHint("Triggers an action when tapped")
    
  • Accessibility Roles:
    Define roles to guide assistive technologies:

    Text("Accessible Text")
        .accessibilityRole(.header)
    
  • Testing Accessibility:
    Use the Accessibility Inspector or VoiceOver to test accessibility configurations.


13. Performance Optimization

Optimizing performance ensures a smooth user experience, especially in data-heavy or animation-intensive apps.

  • Lazy Loading:
    Use lazy containers for large datasets:

    LazyVStack {
        ForEach(0..<1000, id: \.self) { index in
            Text("Item \(index)")
        }
    }
    
  • Minimizing View Rebuilds:
    Reduce unnecessary rebuilds by structuring state management carefully.

  • Profiling:
    Use Xcode Instruments to identify bottlenecks.


14. Using Combine with SwiftUI

Combine is a reactive framework that integrates seamlessly with SwiftUI for handling asynchronous tasks and streams of data.

  • Basic Combine Integration:

    class ViewModel: ObservableObject {
        @Published var data: [String] = []
    
        private var cancellable: AnyCancellable?
    
        func fetchData() {
            cancellable = URLSession.shared.dataTaskPublisher(for: URL(string: "https://example.com")!)
                .map { $0.data }
                .decode(type: [String].self, decoder: JSONDecoder())
                .replaceError(with: [])
                .receive(on: RunLoop.main)
                .assign(to: &$data)
        }
    }
    
  • SwiftUI and Combine Together:
    Bind Combine streams directly to your SwiftUI views:

    struct ContentView: View {
        @StateObject var viewModel = ViewModel()
    
        var body: some View {
            List(viewModel.data, id: \.self) { item in
                Text(item)
            }
            .onAppear {
                viewModel.fetchData()
            }
        }
    }
    
Back to Blog

Related Posts

View All Posts »
Mango Cheesecake Smoothie

Mango Cheesecake Smoothie

Summer means mangoes! This delightfully thick, creamy mango cheesecake milkshake / smoothie will take you back to childhood. 😊