Skip to content

[V5] Use Swift 6 with complete concurrency checking #902

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: v5
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/smoke-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ jobs:

build-xcode15:
name: Build SDKs (Xcode 15)
runs-on: macos-14
if: ${{ github.event.inputs.record_snapshots != 'true' }}
runs-on: macos-15
#if: ${{ github.event.inputs.record_snapshots != 'true' }}
if: false
env:
XCODE_VERSION: "15.4"
steps:
Expand Down
2 changes: 1 addition & 1 deletion .swiftformat
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Stream rules
--header "\nCopyright © {year} Stream.io Inc. All rights reserved.\n"
--swiftversion 5.9
--swiftversion 6.0

--ifdef no-indent
--disable redundantType
Expand Down
4 changes: 0 additions & 4 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,10 @@ only_rules:
- trailing_comma
- trailing_newline
- trailing_semicolon
- trailing_whitespace
Copy link
Contributor Author

@laevandus laevandus Aug 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was breaking markdown tests which only happened in this PR because I did a change in MessageView_Test.swift which then led linter to lint the whole file and apply fixes. Some tests need soft linebreak which is (double space)

- unneeded_break_in_switch
- unneeded_override
- unused_closure_parameter
- unused_control_flow_label
- unused_import
- vertical_whitespace
- void_return

trailing_whitespace:
ignores_empty_lines: true
2 changes: 1 addition & 1 deletion DemoAppSwiftUI/AppConfiguration/AppConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import StreamChat
import StreamChatSwiftUI

final class AppConfiguration {
static let `default` = AppConfiguration()
@MainActor static let `default` = AppConfiguration()

/// The translation language to set on connect.
var translationLanguage: TranslationLanguage?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,17 @@ import SwiftUI
struct AppConfigurationTranslationView: View {
@Environment(\.dismiss) var dismiss

var selection: Binding<TranslationLanguage?> = Binding {
AppConfiguration.default.translationLanguage
} set: { newValue in
AppConfiguration.default.translationLanguage = newValue
}

var body: some View {
List {
ForEach(TranslationLanguage.all, id: \.languageCode) { language in
Button(action: {
selection.wrappedValue = language
AppConfiguration.default.translationLanguage = language
dismiss()
}) {
HStack {
Text(language.languageCode)
Spacer()
if selection.wrappedValue == language {
if AppConfiguration.default.translationLanguage == language {
Image(systemName: "checkmark")
}
}
Expand Down
9 changes: 3 additions & 6 deletions DemoAppSwiftUI/AppConfiguration/AppConfigurationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@ import Combine
import SwiftUI

struct AppConfigurationView: View {
var channelPinningEnabled: Binding<Bool> = Binding {
AppConfiguration.default.isChannelPinningFeatureEnabled
} set: { newValue in
AppConfiguration.default.isChannelPinningFeatureEnabled = newValue
}
@State private var channelPinningEnabled = AppConfiguration.default.isChannelPinningFeatureEnabled

var body: some View {
NavigationView {
Expand All @@ -19,12 +15,13 @@ struct AppConfigurationView: View {
NavigationLink("Translation") {
AppConfigurationTranslationView()
}
Toggle("Channel Pinning", isOn: channelPinningEnabled)
Toggle("Channel Pinning", isOn: $channelPinningEnabled)
}
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("App Configuration")
}
.onChange(of: channelPinningEnabled, perform: { AppConfiguration.default.isChannelPinningFeatureEnabled = $0 })
}
}

Expand Down
4 changes: 2 additions & 2 deletions DemoAppSwiftUI/AppleMessageComposerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ struct AppleMessageComposerView<Factory: ViewFactory>: View, KeyboardReadable {
}
)
.offset(y: -composerHeight)
.animation(nil) : nil,
.animation(.none, value: viewModel.showCommandsOverlay) : nil,
alignment: .bottom
)
.modifier(factory.makeComposerViewModifier())
Expand Down Expand Up @@ -213,7 +213,7 @@ struct BlurredBackground: View {
}

struct HeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat?
static var defaultValue: CGFloat? { nil }

static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
value = value ?? nextValue()
Expand Down
2 changes: 1 addition & 1 deletion DemoAppSwiftUI/ChannelHeader/BlockedUsersViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import StreamChat
import StreamChatSwiftUI
import SwiftUI

class BlockedUsersViewModel: ObservableObject {
@MainActor class BlockedUsersViewModel: ObservableObject {
@Injected(\.chatClient) var chatClient

@Published var blockedUsers = [ChatUser]()
Expand Down
4 changes: 3 additions & 1 deletion DemoAppSwiftUI/ChannelHeader/ChooseChannelQueryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import StreamChatSwiftUI
import SwiftUI

struct ChooseChannelQueryView: View {
static let queryIdentifiers = ChannelListQueryIdentifier.allCases.sorted(using: KeyPathComparator(\.title))
static var queryIdentifiers: [ChannelListQueryIdentifier] {
ChannelListQueryIdentifier.allCases.sorted(by: { $0.title < $1.title })
}

var body: some View {
ForEach(Self.queryIdentifiers) { queryIdentifier in
Expand Down
2 changes: 1 addition & 1 deletion DemoAppSwiftUI/ChannelHeader/NewChatViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import StreamChat
import StreamChatSwiftUI
import SwiftUI

class NewChatViewModel: ObservableObject, ChatUserSearchControllerDelegate {
@MainActor class NewChatViewModel: ObservableObject, ChatUserSearchControllerDelegate {
@Injected(\.chatClient) var chatClient

@Published var searchText: String = "" {
Expand Down
2 changes: 1 addition & 1 deletion DemoAppSwiftUI/CreateGroupView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ struct SearchBar: View {
}
.padding(.trailing, 10)
.transition(.move(edge: .trailing))
.animation(.default)
.animation(.default, value: isEditing)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion DemoAppSwiftUI/CreateGroupViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import StreamChat
import StreamChatSwiftUI
import SwiftUI

class CreateGroupViewModel: ObservableObject, ChatUserSearchControllerDelegate {
@MainActor class CreateGroupViewModel: ObservableObject, ChatUserSearchControllerDelegate {
@Injected(\.chatClient) var chatClient

var channelController: ChatChannelController!
Expand Down
2 changes: 1 addition & 1 deletion DemoAppSwiftUI/DemoAppSwiftUIApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ struct DemoAppSwiftUIApp: App {
}
}

class AppState: ObservableObject, CurrentChatUserControllerDelegate {
@MainActor class AppState: ObservableObject, CurrentChatUserControllerDelegate {
@Injected(\.chatClient) var chatClient: ChatClient

// Recreate the content view when channel query changes.
Expand Down
2 changes: 1 addition & 1 deletion DemoAppSwiftUI/DemoUser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public let apiKeyString = "zcgvnykxsfm8"
public let applicationGroupIdentifier = "group.io.getstream.iOS.ChatDemoAppSwiftUI"
public let currentUserIdRegisteredForPush = "currentUserIdRegisteredForPush"

public struct UserCredentials: Codable {
public struct UserCredentials: Codable, Sendable {
public let id: String
public let name: String
public let avatarURL: URL?
Expand Down
2 changes: 1 addition & 1 deletion DemoAppSwiftUI/LaunchAnimationState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import SwiftUI

class LaunchAnimationState: ObservableObject {
@MainActor class LaunchAnimationState: ObservableObject {
@Published var showAnimation = true

init() {
Expand Down
1 change: 0 additions & 1 deletion DemoAppSwiftUI/LoginView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ struct LoginView: View {
DemoUserView(user: user)
}
.padding(.vertical, 4)
.animation(nil)
}
.listStyle(.plain)

Expand Down
22 changes: 8 additions & 14 deletions DemoAppSwiftUI/LoginViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import StreamChat
import StreamChatSwiftUI
import SwiftUI

class LoginViewModel: ObservableObject {
@MainActor class LoginViewModel: ObservableObject {
@Published var demoUsers = UserCredentials.builtInUsers
@Published var loading = false
@Published var showsConfiguration = false
Expand Down Expand Up @@ -40,13 +40,10 @@ class LoginViewModel: ObservableObject {
log.error("connecting the user failed \(error)")
return
}

DispatchQueue.main.async { [weak self] in
withAnimation {
self?.loading = false
UnsecureRepository.shared.save(user: credentials)
AppState.shared.userState = .loggedIn
}
withAnimation {
self?.loading = false
UnsecureRepository.shared.save(user: credentials)
AppState.shared.userState = .loggedIn
}
}
}
Expand All @@ -62,12 +59,9 @@ class LoginViewModel: ObservableObject {
log.error("connecting the user failed \(error)")
return
}

DispatchQueue.main.async { [weak self] in
withAnimation {
self?.loading = false
AppState.shared.userState = .loggedIn
}
withAnimation {
self?.loading = false
AppState.shared.userState = .loggedIn
}
}
}
Expand Down
38 changes: 19 additions & 19 deletions DemoAppSwiftUI/NotificationsHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import UIKit
/// Handles push notifications in the demo app.
/// When a notification is received, the channel id is extracted from the notification object.
/// The code below shows an example how to use it to navigate directly to the corresponding screen.
class NotificationsHandler: NSObject, ObservableObject, UNUserNotificationCenterDelegate {
@MainActor class NotificationsHandler: NSObject, ObservableObject, UNUserNotificationCenterDelegate {
@Injected(\.chatClient) private var chatClient

@Published var notificationChannelId: String?
Expand All @@ -19,7 +19,7 @@ class NotificationsHandler: NSObject, ObservableObject, UNUserNotificationCenter

override private init() {}

func userNotificationCenter(
nonisolated func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
Expand All @@ -40,25 +40,28 @@ class NotificationsHandler: NSObject, ObservableObject, UNUserNotificationCenter
return
}

if AppState.shared.userState == .loggedIn {
notificationChannelId = cid.description
} else if let userId = UserDefaults(suiteName: applicationGroupIdentifier)?.string(forKey: currentUserIdRegisteredForPush),
let userCredentials = UserCredentials.builtInUsersByID(id: userId),
let token = try? Token(rawValue: userCredentials.token) {
loginAndNavigateToChannel(
userCredentials: userCredentials,
token: token,
cid: cid
)
Task { @MainActor in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have some consistency in how we run things on the main thread. On the other PR this was done with an abstraction.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this piece is not part of the SDK and part of the demo app, I did not want to duplicate here. So demo app uses Task, SDK uses that abstraction (mostly)

if AppState.shared.userState == .loggedIn {
notificationChannelId = cid.description
} else if
let userId = UserDefaults(suiteName: applicationGroupIdentifier)?.string(forKey: currentUserIdRegisteredForPush),
let userCredentials = UserCredentials.builtInUsersByID(id: userId),
let token = try? Token(rawValue: userCredentials.token) {
loginAndNavigateToChannel(
userCredentials: userCredentials,
token: token,
cid: cid
)
}
}
}

func setupRemoteNotifications() {
UNUserNotificationCenter
.current()
.requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
.requestAuthorization(options: [.alert, .sound, .badge]) { @Sendable granted, _ in
if granted {
DispatchQueue.main.async {
Task { @MainActor in
UIApplication.shared.registerForRemoteNotifications()
}
}
Expand All @@ -80,11 +83,8 @@ class NotificationsHandler: NSObject, ObservableObject, UNUserNotificationCenter
log.debug("Error logging in")
return
}

DispatchQueue.main.async {
AppState.shared.userState = .loggedIn
self?.notificationChannelId = cid.description
}
AppState.shared.userState = .loggedIn
self?.notificationChannelId = cid.description
}
}
}
2 changes: 1 addition & 1 deletion DemoAppSwiftUI/UserRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ final class UnsecureRepository: UserRepository {
defaults.object(forKey: key.rawValue) as? T
}

static let shared = UnsecureRepository()
@MainActor static let shared = UnsecureRepository()

func save(user: UserCredentials) {
let encoder = JSONEncoder()
Expand Down
29 changes: 17 additions & 12 deletions DemoAppSwiftUI/ViewFactoryExamples.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,21 +91,23 @@ class DemoAppFactory: ViewFactory {
iconName: "archivebox",
action: { [weak self] in
guard let self else { return }
nonisolated(unsafe) let unsafeOnDismiss = onDismiss
nonisolated(unsafe) let unsafeOnError = onError
let channelController = self.chatClient.channelController(for: channel.cid)
if channel.isArchived {
channelController.unarchive { error in
if let error = error {
onError(error)
unsafeOnError(error)
} else {
onDismiss()
unsafeOnDismiss()
}
}
} else {
channelController.archive { error in
if let error = error {
onError(error)
unsafeOnError(error)
} else {
onDismiss()
unsafeOnDismiss()
}
}
}
Expand All @@ -125,21 +127,23 @@ class DemoAppFactory: ViewFactory {
iconName: "pin.fill",
action: { [weak self] in
guard let self else { return }
nonisolated(unsafe) let unsafeOnDismiss = onDismiss
nonisolated(unsafe) let unsafeOnError = onError
let channelController = self.chatClient.channelController(for: channel.cid)
if channel.isPinned {
channelController.unpin { error in
if let error = error {
onError(error)
unsafeOnError(error)
} else {
onDismiss()
unsafeOnDismiss()
}
}
} else {
channelController.pin { error in
if let error = error {
onError(error)
unsafeOnError(error)
} else {
onDismiss()
unsafeOnDismiss()
}
}
}
Expand Down Expand Up @@ -266,14 +270,15 @@ class CustomFactory: ViewFactory {
onDismiss: onDismiss,
onError: onError
)

let freeze = {
nonisolated(unsafe) let unsafeOnDismiss = onDismiss
nonisolated(unsafe) let unsafeOnError = onError
let freeze: @MainActor @Sendable() -> Void = {
let controller = self.chatClient.channelController(for: channel.cid)
controller.freezeChannel { error in
if let error = error {
onError(error)
unsafeOnError(error)
} else {
onDismiss()
unsafeOnDismiss()
}
}
}
Expand Down
Loading
Loading