SwiftUI for non-Apple platforms (like Android, Web, Windows)

In a way, SwiftUI is really the modern NextStep of the Apple software stack.
The corporate world would love SwiftUI on different platforms.
Steve Jobs would likely approve of it.

Linux has many GUIs, (GNOME, KDE, Xfce etc...)
Each of these teams would need to implement SwiftUI

Microsoft could do it for Windows and Google could do it for Android.

Diversity matters. :slight_smile:

As a reminder, GNOME, KDE and Xfce are window managers and do not inherently need a SwiftUI implementation as they all use x-windows a common backend framework for UI management. What the X-Project was designed for was to standardize how windows are stored and managed.

This means if Alice is on NixOS and bob is on Cinnamon Mint and they both want to have access to the windows on a Arch Machine they will be able to do so as long as they have access to the Arch Machineā€™s x-server because prior to the x project every fork of Linux or Unix copies didnā€™t have guaranteed consistency among their window management systems.

Window managers simply provide the Flourishes and embellishments like the title bar, window close options, window snapping and fullscreen buttons. The x Standard is a long term supported standard that is considered ā€œFeature Completeā€ meaning that if a SwiftUI port was made then you could simply provide support for creating x-windows however some slight considerations may need to be made just to provide buttons or hide the title bar.

Any x-window will feel native on Linux as the local x-server will be merged with the remote x-server and the window manager will draw its flourishes. This means that we can just write one OpenGL or other rendering Library codebase allowing for the ability to rapidly build new 1 for 1 replicas of Features added to SwiftUI. I hope that this makes sense.

Isn't this being developed at Adwaita - Aparoksha

It was featured Swift.org - Writing GNOME Apps with Swift

Sorry if this was already mentioned but I didn't see it with a search.

1 Like

There's also my project SwiftCrossUI which is still heavily in development but uses the native UI framework for each platform (i.e. AppKit on macOS, WinUI on Windows, and Gtk 4 on Linux).

I'm currently working on the layout_system branch which introduces a proper SwiftUI-style layout system behind the scenes (instead of relying on the widely varying layout systems of the various underlying UI frameworks to 'do the right thing'). Unfortunately that means the WinUI backend is currently a bit behind (which I've been fixing the past few days), so I don't have a Windows screenshots of either of the examples included below yet.


I'll likely make a more detailed standalone post on the forums once the WinUI backend is up to scratch again.

Tip calculator example

As an example, here's a basic tip calculator made with SwiftCrossUI running on AppKit and Gtk (not WinUI yet since the example uses some features introduced after the last version of SwiftCrossUI that properly supported Windows, see above). I have some more visually pleasing examples but none with screenshots lying around handy.

I've included the code below the screenshots. As you can see the API still has a few rough edges since I'm mainly focusing on the core implementation and layout system at the moment, but there's nothing too hard to fix.

import SwiftCrossUI
import DefaultBackend
import Foundation

class TipCalculatorState: Observable {
    @Observed var percentage = 0.1
    @Observed var headCount = 1
    @Observed var total = "0"

    var headCountText: String {
        if headCount == 1 {
            "1 person"
        } else {
            "\(headCount) people"
        }
    }

    var totalTip: Double {
        (Double(total) ?? 0) * percentage
    }

    var tipPerPerson: Double {
        totalTip / Double(headCount)
    }
}

@main
struct TipCalculatorApp: App {
    var state = TipCalculatorState()

    var body: some Scene {
        WindowGroup("Tip Calculator") {
            HStack(alignment: .top, spacing: 0) {
                VStack(spacing: 16) {
                    Text("Settings")
                        .font(.system(size: 20))

                    VStack(alignment: .leading, spacing: 4) {
                        Text("Bill")
                        TextField("Total", state.$total)
                    }

                    VStack(alignment: .leading, spacing: 4) {
                        Text("Tip: \(formatPercentage(state.percentage))")
                        Slider(state.$percentage, minimum: 0, maximum: 1)
                    }

                    HStack {
                        Button("-") {
                            state.headCount = max(state.headCount - 1, 1)
                        }
                        Text(state.headCountText)
                        Button("+") {
                            state.headCount = max(state.headCount + 1, 1)
                        }
                    }
                }
                .padding()

                Color.gray.frame(width: 1)

                VStack {
                    Text("Result")
                        .font(.system(size: 20))

                    HStack {
                        VStack(alignment: .leading) {
                            Text("Total: \(formatDollars(state.totalTip))")
                            Text("Per person: \(formatDollars(state.tipPerPerson))")
                        }

                        Spacer()
                    }
                }
                .padding()
            }
            .frame(width: 400, height: 250)
        }
        .windowResizability(.contentSize)
    }

    func formatDollars(_ value: Double) -> String {
        String(format: "$%.02f", value)
    }

    func formatPercentage(_ value: Double) -> String {
        String(format: "%.0f%%", value * 100)
    }
}
24 Likes

Just finished reimplementing enough of the WinUI backend to get the tip calculator example running on Windows;

13 Likes

Whoa, this looks great!

Since it's building to GTK, then apps will run on FreeBSD as well right?

Are you familiar with Clay? I guess it doesn't use GTK but renders straight to RayLib or another renderer of your choice. It seems elegant, wonder if there's ideas to adopt from it for your library https://www.nicbarker.com/clay

Also are you familiar with Skip? They're doing work to get Swift and SwiftUI on Android. Wonder if there's synergy here. https://skip.tools

Really impressive what you've built.

2 Likes

I haven't tried, but it's quite likely!

Haven't heard of it before, but seems quite nice. For now SwiftCrossUI's layout system is pretty similar to SwiftUI's. I haven't looked into the SwiftUI internals myself but I believe I've implemented a similar idea, using kean's great blog post on SwiftUI's layout system as a basic starting point.

Yeah Skip's approach is quite interesting, from conversations I've had with others I got the sense that they're transpiling Swift to Kotlin?

I have plans to begin on basic Android support in the near future and have an approach layed out that I believe will work. It'll be quite different to the Skip approach with its own pros and cons. I shouldn't get ahead of myself though, iOS is next on my list due to the similarities between AppKit and UIKit.

Thanks! :pray: I appreciate it

2 Likes

very cool; yeah they're doing transpiling, so it uses JetPack compose natively on Android. And then now they can cross compile some swift code directly so business logic just gets compiled natively on android. It seems like a good way so each platform feels native.

Huh, whats your motivation for iOS and MacOS support? I would think just stick with native swiftUI there? and in the code just create separate views for linux and windows with your library, to keep each platform feeling native? or is the idea you just want to write once run everywhere and your library will wrap SwiftUI on apple platforms?

My library has (and will always have) some differences with SwiftUI, so I decided that it's best to implement backends on all platforms instead of deferring to SwiftUI on Apple platforms. There are many SwiftUI features and bug fixes in each OS release, and developers can't make use of them until the required OS is sufficiently old. But with SwiftCrossUI I've implemented macOS support on top of AppKit so the exact same features can be deployed to all platforms that you target with SwiftCrossUI.

The way I've set things up, SwiftCrossUI's public API is separate from the backend implementations for each platform. I want to avoid the nasty surprise that would occur when testing a macOS-centric SwiftCrossUI app on Linux if SwiftCrossUI were to just defer directly to SwiftUI on Apple platforms. It'd be quite tedious for developers to always check exactly what is and isn't implemented in SwiftCrossUI without the help of autocomplete.

The library directly wraps AppKit because wrapping SwiftUI itself would actually be significantly harder unless I just cut out all of the SwiftCrossUI internals when on Apple platforms. And that wouldn't be great because then the SwiftUI layout system would be used on Apple platforms while the SwiftCrossUI layout system would be used everywhere else, inevitably leading to differing behaviour and inconsistencies.

Hope that helps explain my thinking behind that decision a bit? Please feel free to continue with the questions if you have any more though!

8 Likes

Wow! It makes sense now that you explain it, just sounds like a lot of work, but if you can do it, great! So for iOS you will go directly to UIKit then?

This is really interesting to me.. I've been playing around with an idea of a SwiftOS SwiftOS, a proposal - #38 by MadeByDouglas and thinking how it could be more than just a cool swift project, i.e. practical improvements over existing OSs with a chance of real world adoption. Something technically sound and with a whole new UX paradigm centered around composable, agentic widgets, instead of isolated apps.

After a lot of talking with people and research, looking at Linux, FreeBSD, XNU, RedoxOS, someone reminded me about FuchsiaOS and when I looked at it, seems to me it is the future OS I imagine, a micro kernel, secure, high performant, replacement for Android (no more Java) and Linux, and curiously enough inspired by iOS (and beOS.)

I think the way forward for SwiftOS is that it starts as a Fuchsia desktop environment written in swift and to allow swift applications to run on it. They already have GTK bindings and effort to run Linux apps, but we can also talk to the compositor, Scenic, and their 2d renderer, Flatland, directly, which is how their default implementation in Flutter works. Scenic, the Fuchsia system compositor

I think your SwiftCrossUI library would be the perfect use case for this, as we'd add Fuchsia as another backend. I'm also wondering if we can make easy to reuse widgets like how Streamlit works in Python. Would you be interested in collaborating on this?

1 Like

Yeah exactly!

I generally keep myself pretty busy with my various personal projects (SwiftCrossUI included), but I'd be happy to help from the SwiftCrossUI/Swift Bundler side of things.

1 Like

This looks like ObservableObject approach. Have you considered @Observable approach and decided it is not right for you?

1 Like

Itā€™s more just that I havenā€™t gotten around to that yet. I started the project before @Observable was introduced and havenā€™t touched state management a whole lot since. Iā€™ll be introducing @State soon, and then @Observable will definitely be on my todos.

2 Likes

Totally understand, cool, will keep in touch via the discord group

1 Like

Just finished introducing @State based state management :tada:

import SwiftCrossUI
import DefaultBackend

@main
struct CounterApp: App {
    @State var count = 0

    var body: some Scene {
        WindowGroup("CounterApp") {
            HStack {
                Button("-") {
                    count -= 1
                }
                Text("Count: \(count)")
                Button("+") {
                  count += 1
                }
            }.padding(10)
        }
    }
}
4 Likes

Consider the approach of making it more compatible with SwiftUI on the API level (e.g. @Published instead of @Observed, etc). The benefit would be that users would be able to simply change a single line "import SwiftCrossUI" to "import SwiftUI" to test how it compares to SwiftUI on apple platforms.


Great project, thank you for exploring this.

2 Likes

Thanks for the feedback! I've now renamed Observable to ObservableObject, and Observed to Published to be more in line with SwiftUI's equivalent APIs.

3 Likes

And, with UIKit backend by bbrk24 Ā· Pull Request #98 Ā· stackotter/swift-cross-ui Ā· GitHub being merged, iOS (and Apple TV) is now supported!

4 Likes