It's been a long, long while since I last looked at doing any development for mobile devices. My experiences were all disappointing and negative. While I could get things to build, they always seemed to have issues and installing onto an actual device was a nightmare of versioning and incompatibility. This was all based on Android as I have had an android phone and was writing small apps for myself.

Things have changed and now I use an Apple device at work and there are spiffy new ways of writing mobile apps (or so the hype says) so I decided to have another look.

To try and compare I decided to write a very simple small app that would allow me to gain the experience for the first real app I want to write. I've tried to do it the same way on SwiftUI and Jetpack Compose, using XCode and AndroidStudio respectively.

The code that follows works but is very much a first attempt. I've included it to allow a comparison between the 2 approaches. They share a lot of similarities which makes things simpler but the differences are also apparent. I'll try to make the code easy to read :-)

Screenshots of what each app looks like in the previews are shown below the code. I have tried to make them as similar as I could.

Jetpack Compose

package com.example.rect2application

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.rect2application.ui.theme.Rect2ApplicationTheme

class RectViewModel: ViewModel() {
    private var _width = mutableStateOf(20)
    private var _height = mutableStateOf(10)

    val width:State<Int> = _width
    val height:State<Int> = _height

    fun area():Int {
        return _width.value * _height.value
    }
    fun length():Int {
        return 2 * _width.value + 2 * _height.value
    }
    fun setWidth(w:Int) {
        _width.value = w
    }
    fun setHeight(h:Int) {
        _height.value = h
    }
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Rect2ApplicationTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    DataForm()
                }
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SimpleTextFieldSample(label:String, dflt:Int, 
                          onChange: (a:Int) -> Unit) {
    var text by rememberSaveable { mutableStateOf(dflt.toString()) }

    Row(modifier= Modifier
        .fillMaxWidth()
        .padding(10.dp, 5.dp)
        ,
        verticalAlignment = Alignment.CenterVertically) {
        Text(label,
            style=MaterialTheme.typography.bodyLarge,
            textAlign = TextAlign.Center
            )
        Spacer(Modifier.weight(1F))
        OutlinedTextField(
            value = text,
            onValueChange = { raw ->
                text = raw
                val parsed = raw.toIntOrNull() ?: 0
                onChange(parsed)
            },
            label = {},
            singleLine = true,
            modifier = Modifier
                .width(100.dp)
                .padding(0.dp),
            textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.End),
            keyboardOptions = KeyboardOptions(
                keyboardType = KeyboardType.Number,
            ),
        )
    }
}

@Preview(showBackground = true)
@Composable
fun SimpleTextFieldSamplePreview() {
    SimpleTextFieldSample(label = "Sample", dflt=80, onChange = {})
}

@Composable
fun DataForm(rectViewData:RectViewModel = viewModel()) {
    Column(modifier = Modifier.fillMaxSize()) {
        Text(text="Rectangle Demo",
            modifier=Modifier.align(Alignment.CenterHorizontally)
                .padding(10.dp, 20.dp),
            style=MaterialTheme.typography.headlineMedium)
        SimpleTextFieldSample(label = "Width", 
            dflt = rectViewData.width.value,
            rectViewData::setWidth)
        SimpleTextFieldSample(label = "Height", 
            dflt = rectViewData.height.value, 
            rectViewData::setHeight)
        Text("Area = ${rectViewData.area()}",
            modifier=Modifier.align(Alignment.CenterHorizontally)
                .padding(10.dp),
            style=MaterialTheme.typography.titleLarge)
        Text("Length = ${rectViewData.length()}",
            modifier=Modifier.align(Alignment.CenterHorizontally)
                .padding(10.dp),
            style=MaterialTheme.typography.titleLarge)
    }
}

@Preview(showBackground = true)
@Composable
fun DataFormPreview() {
    DataForm()
}
Screenshot of app running

SwiftUI

I'd never used Swift until this project and while I have found a lot of people complaining about the Apple tutorials, I found them to be helpful. In fact I found it simpler to get answers to most of my questions around SwiftUI online than I did for Android.

import SwiftUI

@main
struct rectApplicationApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
import SwiftUI

struct rectData {
    var width:Int = 20
    var height:Int = 10
    
    var area:Int {
        width * height
    }
    
    var length:Int {
        2 * width + 2 * height
    }
}

struct ContentView: View {
    @State var rd:rectData = rectData()
    var body: some View {
        VStack {
            DataForm(rd: $rd)
            Spacer()
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

struct DataForm: View {
    @Binding var rd:rectData
    var body: some View {
        VStack {
            Text("Rectangle Demo")
                .fontWeight(.bold)
            InputLine(I: $rd.width, Title:"Width")
            InputLine(I: $rd.height, Title:"Height")
            Text("Area = \(rd.area)")
            Text("Length = \(rd.length)")
        }
    }
}

struct DataForm_Preview:PreviewProvider {
    @State static var rd:rectData = rectData(width: 20, height: 10)
    static var previews: some View {
        return DataForm(rd: $rd)
    }
}


struct InputLine: View {
    var I: Binding<Int>
    @State var Title: String
    
    var body: some View {
        HStack() {
            Text(Title)
            Spacer()
                TextField("",
                          value: I,
                          format: .number)
            .frame(width: 70, height: 20)
            .multilineTextAlignment(.trailing)
            .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
            .background(Color.white)
            .overlay(RoundedRectangle(cornerRadius: 8)
                .stroke(Color.blue, lineWidth: 2).foregroundColor(.blue))
            .shadow(color: Color.gray.opacity(0.4), radius: 3, x: 1, y:2)
        }
    }
}

struct InputLine_Preview:PreviewProvider {
    @State static var rd:rectData = rectData(width: 20, height: 10)
    static var previews: some View {
        return InputLine(I: $rd.width, Title: "Width")
    }
}

Conclusion

Compared with my last experience with mobile development, this time around has been MUCH easier. While they may not be for everyone, the declarative nature of SwiftUI and Compose have made a huge impact and suddenly writing small apps seems to be within reach. I still have a huge amount to understand!

XCode is not really my taste for an IDE, but the integrations it offers are difficult to beat.

AndroidStudio is marginally better than last time I used it. The Compose integration is a little clunky but works and the live mirroring to a physical device works well. However, once again I find that the hell that is Android versioning is still present. My device is running SDK version 31, but I can't get a build of the code to work for that SDK version. I'm guessing it's the newest versions of Jetpack/androidx/Compose but finding information on which versions are safe has proven to be impossible thus far. If anyone knows, let me know!

The code shown above is far from complete and represents a few hours of experimentation to give me something to compare and contrast. Maybe it'll be useful for others following a similar path.

Improvements and suggestions for better ways of doing things more than welcome.