Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .github/workflows/crowdin-upload.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
branches: [master]
paths:
- "**/values/strings.xml"
- "iosApp/flare/Localizable.xcstrings"
- "appleApp/ios/Localizable.xcstrings"

jobs:
crowdin:
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ jobs:
- name: Install XcodeGen
run: brew install xcodegen
- name: Generate Xcode project
run: xcodegen generate --spec iosApp/project.yml
run: xcodegen generate --spec appleApp/project.yml
- name: Build
working-directory: ./iosApp
working-directory: ./appleApp
env:
scheme: Flare
scheme: iOS
platform: ${{ 'iOS Simulator' }}
run: |
# xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959)
Expand Down
10 changes: 5 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ local.properties
signing.properties

build/
iosApp/Podfile.lock
iosApp/Pods/*
iosApp/Flare.xcworkspace/*
iosApp/Flare.xcodeproj/*
*/Podfile.lock
*/Pods/*
*/Flare.xcworkspace/*
*/Flare.xcodeproj/*
shared/shared.podspec
.kotlin
.history
.lh
iosApp/K_D/
appleApp/K_D/
*.sh
!.github/scripts/*.sh
.env
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Flare uses [ktlint](https://github.com/pinterest/ktlint) to check the code style
### iOS
- Make sure you have JDK 25 installed
- Make sure you have a Mac with Xcode 26 installed
- open `iosApp/Flare.xcodeproj` in Xcode
- open `appleApp/Flare.xcodeproj` in Xcode
- Build and run the app

### Desktop
Expand All @@ -46,10 +46,10 @@ The project is split into theses parts:
- `shared`: The common code, including bussiness logic.
- `compose-ui`: The Compose UI code that shared between Android, iOS, Desktop.
- `app`: The Android app.
- `iosApp`: The iOS app.
- `appleApp`: The iOS and macOS apps.
- `desktopApp`: The desktop app for Windows/macOS.

Most of the business logic is in `shared`, and the platform specific code and UI is in `app` and `iosApp`.
Most of the business logic is in `shared`, and the platform specific code and UI is in `app` and `appleApp`.
Flare uses [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) to share code between platforms, [Jetpack Compose](https://developer.android.com/jetpack/compose) for the UI on Android, [SwiftUI](https://developer.apple.com/xcode/swiftui/) for the UI on iOS.

### Business logic
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ Here're some features we're planning to implement in the future.
- Make sure you have JDK 25 installed
- Make sure you have a Mac with Xcode 26 installed
- Install XcodeGen with `brew install xcodegen`
- Run `xcodegen generate --spec iosApp/project.yml`
- open `iosApp/Flare.xcodeproj` in Xcode
- Run `xcodegen generate --spec appleApp/project.yml`
- open `appleApp/Flare.xcodeproj` in Xcode
- Build and run the app

### Desktop
Expand Down
57 changes: 39 additions & 18 deletions ios-shared/build.gradle.kts → apple-shared/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import co.touchlab.skie.configuration.DefaultArgumentInterop
import dev.dimension.flare.buildlogic.FlarePlatform
import dev.dimension.flare.buildlogic.flare
import co.touchlab.skie.configuration.DefaultArgumentInterop
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget

plugins {
Expand All @@ -13,31 +13,47 @@ plugins {

kotlin {
flare {
namespace = "dev.dimension.flare.ios.shared"
namespace = "dev.dimension.flare.apple.shared"
platforms(
FlarePlatform.IOS,
FlarePlatform.MACOS,
)
}

listOf("iosArm64", "iosSimulatorArm64")
val commonExportedProjects =
listOf(
projects.shared,
projects.social.bluesky,
projects.social.mastodon,
projects.social.misskey,
projects.social.pixiv,
projects.social.vvo,
projects.social.xqt,
projects.feature.loginApi,
projects.feature.login,
projects.feature.subscription,
projects.feature.tab,
)

listOf("iosArm64", "iosSimulatorArm64", "macosArm64")
.map { targetName -> targets.getByName(targetName) as KotlinNativeTarget }
.forEach { appleTarget ->
appleTarget.binaries.framework {
baseName = "KotlinSharedUI"
isStatic = true
export(projects.shared)
export(projects.social.bluesky)
export(projects.social.mastodon)
export(projects.social.misskey)
export(projects.social.nostr)
export(projects.social.pixiv)
export(projects.social.vvo)
export(projects.social.xqt)
export(projects.feature.loginApi)
export(projects.feature.agent)
export(projects.feature.login)
export(projects.feature.subscription)
export(projects.feature.tab)

if (appleTarget.name.startsWith("macos")) {
linkerOpts.add("-lsqlite3")
}

commonExportedProjects.forEach { exportedProject ->
export(exportedProject)
}

if (appleTarget.name.startsWith("ios")) {
export(projects.social.nostr)
export(projects.feature.agent)
}
}
}

Expand All @@ -48,11 +64,10 @@ kotlin {
api(projects.social.bluesky)
api(projects.social.mastodon)
api(projects.social.misskey)
api(projects.social.nostr)
api(projects.social.pixiv)
api(projects.social.vvo)
api(projects.social.xqt)
api(projects.feature.agent)
api(projects.feature.loginApi)
api(projects.feature.login)
api(projects.feature.subscription)
api(projects.feature.tab)
Expand All @@ -64,6 +79,12 @@ kotlin {
implementation(libs.kotlinx.immutable)
}
}
val iosMain by getting {
dependencies {
api(projects.social.nostr)
api(projects.feature.agent)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
package dev.dimension.flare.ios.shared
package dev.dimension.flare.apple.shared

import dev.dimension.flare.common.InAppNotification
import dev.dimension.flare.common.Message
import dev.dimension.flare.common.SwiftOnDeviceAI
import dev.dimension.flare.data.platform.AllRssTimelineLoaderFactory
import dev.dimension.flare.data.platform.BlueskyPlatformSpec
import dev.dimension.flare.data.platform.MastodonPlatformSpec
import dev.dimension.flare.data.platform.MisskeyPlatformSpec
import dev.dimension.flare.data.platform.NostrPlatformSpec
import dev.dimension.flare.data.platform.PixivPlatformSpec
import dev.dimension.flare.data.platform.RssTimelineSpecs
import dev.dimension.flare.data.platform.VvoPlatformSpec
import dev.dimension.flare.data.platform.XqtPlatformSpec
import dev.dimension.flare.model.PlatformRuntimeData
import dev.dimension.flare.model.PlatformSpec
import dev.dimension.flare.ui.humanizer.SwiftFormatter
import dev.dimension.flare.ui.render.SwiftPlatformTextRenderer
import kotlinx.coroutines.CoroutineScope
Expand All @@ -27,66 +21,59 @@ import org.koin.core.annotation.Single
import org.koin.plugin.module.dsl.startKoin
import kotlin.native.HiddenFromObjC

public object IosSharedHelper {
public object AppleSharedHelper {
public fun initialize(
inAppNotification: InAppNotification,
swiftFormatter: SwiftFormatter,
swiftPlatformTextRenderer: SwiftPlatformTextRenderer,
swiftOnDeviceAI: SwiftOnDeviceAI,
) {
IosBridgeDependencies.install(
AppleBridgeDependencies.install(
inAppNotification = inAppNotification,
swiftFormatter = swiftFormatter,
swiftPlatformTextRenderer = swiftPlatformTextRenderer,
swiftOnDeviceAI = swiftOnDeviceAI,
)
startKoin<IosKoinApplication>()
startKoin<AppleKoinApplication>()
}
}

@HiddenFromObjC
@KoinApplication
internal class IosKoinApplication
internal class AppleKoinApplication

@HiddenFromObjC
@Module
@Configuration
@ComponentScan("dev.dimension.flare.ios.shared")
internal class IosKoinModule
@ComponentScan("dev.dimension.flare.apple.shared")
internal class AppleKoinModule

@Single
internal fun runtimeData(allRssTimelineLoaderFactory: AllRssTimelineLoaderFactory): PlatformRuntimeData =
PlatformRuntimeData(
platformSpecs =
listOf(
NostrPlatformSpec,
MastodonPlatformSpec,
MisskeyPlatformSpec,
BlueskyPlatformSpec,
PixivPlatformSpec,
XqtPlatformSpec,
VvoPlatformSpec,
),
platformSpecs = platformSpecs(),
extraTimelineSpecs = RssTimelineSpecs.timelineSpecs(allRssTimelineLoaderFactory),
)

internal expect fun platformSpecs(): List<PlatformSpec>

@Single
internal fun inAppNotification(scope: CoroutineScope): InAppNotification =
ProxyInAppNotification(
delegate = IosBridgeDependencies.inAppNotification(),
delegate = AppleBridgeDependencies.inAppNotification(),
scope = scope,
)

@Single
internal fun swiftFormatter(): SwiftFormatter = IosBridgeDependencies.swiftFormatter()
internal fun swiftFormatter(): SwiftFormatter = AppleBridgeDependencies.swiftFormatter()

@Single
internal fun swiftPlatformTextRenderer(): SwiftPlatformTextRenderer = IosBridgeDependencies.swiftPlatformTextRenderer()
internal fun swiftPlatformTextRenderer(): SwiftPlatformTextRenderer = AppleBridgeDependencies.swiftPlatformTextRenderer()

@Single
internal fun swiftOnDeviceAI(): SwiftOnDeviceAI = IosBridgeDependencies.swiftOnDeviceAI()
internal fun swiftOnDeviceAI(): SwiftOnDeviceAI = AppleBridgeDependencies.swiftOnDeviceAI()

private object IosBridgeDependencies {
private object AppleBridgeDependencies {
private var inAppNotification: InAppNotification? = null
private var swiftFormatter: SwiftFormatter? = null
private var swiftPlatformTextRenderer: SwiftPlatformTextRenderer? = null
Expand All @@ -105,18 +92,18 @@ private object IosBridgeDependencies {
}

fun inAppNotification(): InAppNotification =
requireNotNull(inAppNotification) { "IosSharedHelper.initialize must install InAppNotification before Koin starts." }
requireNotNull(inAppNotification) { "AppleSharedHelper.initialize must install InAppNotification before Koin starts." }

fun swiftFormatter(): SwiftFormatter =
requireNotNull(swiftFormatter) { "IosSharedHelper.initialize must install SwiftFormatter before Koin starts." }
requireNotNull(swiftFormatter) { "AppleSharedHelper.initialize must install SwiftFormatter before Koin starts." }

fun swiftPlatformTextRenderer(): SwiftPlatformTextRenderer =
requireNotNull(swiftPlatformTextRenderer) {
"IosSharedHelper.initialize must install SwiftPlatformTextRenderer before Koin starts."
"AppleSharedHelper.initialize must install SwiftPlatformTextRenderer before Koin starts."
}

fun swiftOnDeviceAI(): SwiftOnDeviceAI =
requireNotNull(swiftOnDeviceAI) { "IosSharedHelper.initialize must install SwiftOnDeviceAI before Koin starts." }
requireNotNull(swiftOnDeviceAI) { "AppleSharedHelper.initialize must install SwiftOnDeviceAI before Koin starts." }
}

private class ProxyInAppNotification(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package dev.dimension.flare.apple.shared

import dev.dimension.flare.data.platform.BlueskyPlatformSpec
import dev.dimension.flare.data.platform.MastodonPlatformSpec
import dev.dimension.flare.data.platform.MisskeyPlatformSpec
import dev.dimension.flare.data.platform.NostrPlatformSpec
import dev.dimension.flare.data.platform.PixivPlatformSpec
import dev.dimension.flare.data.platform.VvoPlatformSpec
import dev.dimension.flare.data.platform.XqtPlatformSpec
import dev.dimension.flare.model.PlatformSpec

internal actual fun platformSpecs(): List<PlatformSpec> =
listOf(
NostrPlatformSpec,
MastodonPlatformSpec,
MisskeyPlatformSpec,
BlueskyPlatformSpec,
PixivPlatformSpec,
XqtPlatformSpec,
VvoPlatformSpec,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.dimension.flare.apple.shared

import dev.dimension.flare.data.platform.BlueskyPlatformSpec
import dev.dimension.flare.data.platform.MastodonPlatformSpec
import dev.dimension.flare.data.platform.MisskeyPlatformSpec
import dev.dimension.flare.data.platform.PixivPlatformSpec
import dev.dimension.flare.data.platform.VvoPlatformSpec
import dev.dimension.flare.data.platform.XqtPlatformSpec
import dev.dimension.flare.model.PlatformSpec

internal actual fun platformSpecs(): List<PlatformSpec> =
listOf(
MastodonPlatformSpec,
MisskeyPlatformSpec,
BlueskyPlatformSpec,
PixivPlatformSpec,
XqtPlatformSpec,
VvoPlatformSpec,
)
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import SwiftUI
import KotlinSharedUI
import SwiftUI

public final class AppleUriLauncher: UriLauncher {
private let openUrl: OpenURLAction

class AppleUriLauncher: UriLauncher {
let openUrl: OpenURLAction
init(openUrl: OpenURLAction) {
public init(openUrl: OpenURLAction) {
self.openUrl = openUrl
}

func launch(uri: String) {
public func launch(uri: String) {
if let url = URL(string: uri) {
openUrl.callAsFunction(url)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import Foundation
import KotlinSharedUI

class Formatter: SwiftFormatter {
public final class Formatter: SwiftFormatter, @unchecked Sendable {
private init() {}

static let shared = Formatter()
nonisolated func formatNumber(number: Int64) -> String {
return Int(number)

public static let shared = Formatter()

nonisolated public func formatNumber(number: Int64) -> String {
Int(number)
.formatted(
.number
.notation(.compactName)
Expand Down
Loading
Loading