Skip to content

Commit f028d63

Browse files
authored
docs: Slack Clone Example (#235)
* Improving slack clone example * Fix online/offline status
1 parent fd4984c commit f028d63

File tree

11 files changed

+453
-228
lines changed

11 files changed

+453
-228
lines changed

Examples/Examples.xcodeproj/project.pbxproj

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,18 @@
2828
796298992AEBBA77000AA957 /* MFAFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796298982AEBBA77000AA957 /* MFAFlow.swift */; };
2929
7962989D2AEBC6F9000AA957 /* SVGView in Frameworks */ = {isa = PBXBuildFile; productRef = 7962989C2AEBC6F9000AA957 /* SVGView */; };
3030
79719ECE2ADF26C400737804 /* Supabase in Frameworks */ = {isa = PBXBuildFile; productRef = 79719ECD2ADF26C400737804 /* Supabase */; };
31-
797D664A2B46A1D8007592ED /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 797D66492B46A1D8007592ED /* Store.swift */; };
31+
797D664A2B46A1D8007592ED /* Dependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 797D66492B46A1D8007592ED /* Dependencies.swift */; };
3232
7993B8A92B3C673A009B610B /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7993B8A82B3C673A009B610B /* AuthView.swift */; };
3333
7993B8AB2B3C67E0009B610B /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7993B8AA2B3C67E0009B610B /* Toast.swift */; };
3434
79AF047F2B2CE207008761AD /* AuthWithEmailAndPassword.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79AF047E2B2CE207008761AD /* AuthWithEmailAndPassword.swift */; };
3535
79AF04812B2CE261008761AD /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79AF04802B2CE261008761AD /* AuthView.swift */; };
3636
79AF04842B2CE408008761AD /* AuthWithMagicLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79AF04832B2CE408008761AD /* AuthWithMagicLink.swift */; };
3737
79AF04862B2CE586008761AD /* Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79AF04852B2CE586008761AD /* Debug.swift */; };
38+
79B8F4242B5FED7C0000E839 /* IdentifiedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 79B8F4232B5FED7C0000E839 /* IdentifiedCollections */; };
39+
79B8F4262B602F640000E839 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79B8F4252B602F640000E839 /* Logger.swift */; };
3840
79BD76772B59C3E300CA3D68 /* UserStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD76762B59C3E300CA3D68 /* UserStore.swift */; };
39-
79BD76792B59C53900CA3D68 /* ChannelsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD76782B59C53900CA3D68 /* ChannelsViewModel.swift */; };
40-
79BD767B2B59C61300CA3D68 /* MessagesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD767A2B59C61300CA3D68 /* MessagesViewModel.swift */; };
41+
79BD76792B59C53900CA3D68 /* ChannelStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD76782B59C53900CA3D68 /* ChannelStore.swift */; };
42+
79BD767B2B59C61300CA3D68 /* MessageStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD767A2B59C61300CA3D68 /* MessageStore.swift */; };
4143
79D884CA2B3C18830009EA4A /* SlackCloneApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79D884C92B3C18830009EA4A /* SlackCloneApp.swift */; };
4244
79D884CC2B3C18830009EA4A /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79D884CB2B3C18830009EA4A /* AppView.swift */; };
4345
79D884CE2B3C18840009EA4A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 79D884CD2B3C18840009EA4A /* Assets.xcassets */; };
@@ -80,17 +82,18 @@
8082
795640692955AFBD0088A06F /* ErrorText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorText.swift; sourceTree = "<group>"; };
8183
796298982AEBBA77000AA957 /* MFAFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MFAFlow.swift; sourceTree = "<group>"; };
8284
7962989A2AEBBD9F000AA957 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
83-
797D66492B46A1D8007592ED /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
85+
797D66492B46A1D8007592ED /* Dependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dependencies.swift; sourceTree = "<group>"; };
8486
7993B8A82B3C673A009B610B /* AuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthView.swift; sourceTree = "<group>"; };
8587
7993B8AA2B3C67E0009B610B /* Toast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toast.swift; sourceTree = "<group>"; };
8688
7993B8AC2B3C97B6009B610B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
8789
79AF047E2B2CE207008761AD /* AuthWithEmailAndPassword.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthWithEmailAndPassword.swift; sourceTree = "<group>"; };
8890
79AF04802B2CE261008761AD /* AuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthView.swift; sourceTree = "<group>"; };
8991
79AF04832B2CE408008761AD /* AuthWithMagicLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthWithMagicLink.swift; sourceTree = "<group>"; };
9092
79AF04852B2CE586008761AD /* Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debug.swift; sourceTree = "<group>"; };
93+
79B8F4252B602F640000E839 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
9194
79BD76762B59C3E300CA3D68 /* UserStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStore.swift; sourceTree = "<group>"; };
92-
79BD76782B59C53900CA3D68 /* ChannelsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelsViewModel.swift; sourceTree = "<group>"; };
93-
79BD767A2B59C61300CA3D68 /* MessagesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesViewModel.swift; sourceTree = "<group>"; };
95+
79BD76782B59C53900CA3D68 /* ChannelStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelStore.swift; sourceTree = "<group>"; };
96+
79BD767A2B59C61300CA3D68 /* MessageStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageStore.swift; sourceTree = "<group>"; };
9497
79D884C72B3C18830009EA4A /* SlackClone.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SlackClone.app; sourceTree = BUILT_PRODUCTS_DIR; };
9598
79D884C92B3C18830009EA4A /* SlackCloneApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlackCloneApp.swift; sourceTree = "<group>"; };
9699
79D884CB2B3C18830009EA4A /* AppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = "<group>"; };
@@ -132,6 +135,7 @@
132135
buildActionMask = 2147483647;
133136
files = (
134137
79D884D92B3C18E90009EA4A /* Supabase in Frameworks */,
138+
79B8F4242B5FED7C0000E839 /* IdentifiedCollections in Frameworks */,
135139
);
136140
runOnlyForDeploymentPostprocessing = 0;
137141
};
@@ -234,10 +238,11 @@
234238
79D884DC2B3C19320009EA4A /* MessagesView.swift */,
235239
7993B8A82B3C673A009B610B /* AuthView.swift */,
236240
7993B8AA2B3C67E0009B610B /* Toast.swift */,
237-
797D66492B46A1D8007592ED /* Store.swift */,
241+
797D66492B46A1D8007592ED /* Dependencies.swift */,
238242
79BD76762B59C3E300CA3D68 /* UserStore.swift */,
239-
79BD76782B59C53900CA3D68 /* ChannelsViewModel.swift */,
240-
79BD767A2B59C61300CA3D68 /* MessagesViewModel.swift */,
243+
79BD76782B59C53900CA3D68 /* ChannelStore.swift */,
244+
79BD767A2B59C61300CA3D68 /* MessageStore.swift */,
245+
79B8F4252B602F640000E839 /* Logger.swift */,
241246
);
242247
path = SlackClone;
243248
sourceTree = "<group>";
@@ -318,6 +323,7 @@
318323
name = SlackClone;
319324
packageProductDependencies = (
320325
79D884D82B3C18E90009EA4A /* Supabase */,
326+
79B8F4232B5FED7C0000E839 /* IdentifiedCollections */,
321327
);
322328
productName = SlackClone;
323329
productReference = 79D884C72B3C18830009EA4A /* SlackClone.app */;
@@ -453,10 +459,11 @@
453459
7993B8A92B3C673A009B610B /* AuthView.swift in Sources */,
454460
7993B8AB2B3C67E0009B610B /* Toast.swift in Sources */,
455461
79D884DD2B3C19320009EA4A /* MessagesView.swift in Sources */,
456-
79BD76792B59C53900CA3D68 /* ChannelsViewModel.swift in Sources */,
457-
797D664A2B46A1D8007592ED /* Store.swift in Sources */,
462+
79BD76792B59C53900CA3D68 /* ChannelStore.swift in Sources */,
463+
797D664A2B46A1D8007592ED /* Dependencies.swift in Sources */,
458464
79D884DB2B3C191F0009EA4A /* ChannelListView.swift in Sources */,
459-
79BD767B2B59C61300CA3D68 /* MessagesViewModel.swift in Sources */,
465+
79BD767B2B59C61300CA3D68 /* MessageStore.swift in Sources */,
466+
79B8F4262B602F640000E839 /* Logger.swift in Sources */,
460467
79D884CC2B3C18830009EA4A /* AppView.swift in Sources */,
461468
79BD76772B59C3E300CA3D68 /* UserStore.swift in Sources */,
462469
79D884D72B3C18DB0009EA4A /* Supabase.swift in Sources */,
@@ -911,6 +918,11 @@
911918
isa = XCSwiftPackageProductDependency;
912919
productName = Supabase;
913920
};
921+
79B8F4232B5FED7C0000E839 /* IdentifiedCollections */ = {
922+
isa = XCSwiftPackageProductDependency;
923+
package = 7956406E2955B5190088A06F /* XCRemoteSwiftPackageReference "swift-identified-collections" */;
924+
productName = IdentifiedCollections;
925+
};
914926
79D884D82B3C18E90009EA4A /* Supabase */ = {
915927
isa = XCSwiftPackageProductDependency;
916928
productName = Supabase;

Examples/SlackClone/ChannelListView.swift

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,25 @@ import SwiftUI
99

1010
@MainActor
1111
struct ChannelListView: View {
12-
let store = Store.shared.channel
12+
@Bindable var store = Dependencies.shared.channel
1313
@Binding var channel: Channel?
1414

15+
@State private var addChannelPresented = false
16+
@State private var newChannelName = ""
17+
1518
var body: some View {
1619
List(store.channels, selection: $channel) { channel in
1720
NavigationLink(channel.slug, value: channel)
1821
}
1922
.toolbar {
23+
ToolbarItem(placement: .primaryAction) {
24+
Button("Add Channel") {
25+
addChannelPresented = true
26+
}
27+
.popover(isPresented: $addChannelPresented) {
28+
addChannelView
29+
}
30+
}
2031
ToolbarItem {
2132
Button("Log out") {
2233
Task {
@@ -25,5 +36,26 @@ struct ChannelListView: View {
2536
}
2637
}
2738
}
39+
.toast(state: $store.toast)
40+
}
41+
42+
private var addChannelView: some View {
43+
Form {
44+
Section {
45+
TextField("New channel name", text: $newChannelName)
46+
}
47+
48+
Section {
49+
Button("Add") {
50+
Task {
51+
await store.addChannel(newChannelName)
52+
addChannelPresented = false
53+
}
54+
}
55+
}
56+
}
57+
#if os(macOS)
58+
.padding()
59+
#endif
2860
}
2961
}

Examples/SlackClone/ChannelsViewModel.swift renamed to Examples/SlackClone/ChannelStore.swift

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// ChannelsViewModel.swift
2+
// ChannelStore.swift
33
// SlackClone
44
//
55
// Created by Guilherme Souza on 18/01/24.
@@ -8,20 +8,19 @@
88
import Foundation
99
import Supabase
1010

11-
protocol ChannelsStore: AnyObject {
12-
func fetchChannel(id: Channel.ID) async throws -> Channel
13-
}
14-
1511
@MainActor
1612
@Observable
17-
final class ChannelsViewModel: ChannelsStore {
13+
final class ChannelStore {
14+
static let shared = ChannelStore()
15+
1816
private(set) var channels: [Channel] = []
17+
var toast: ToastState?
1918

20-
weak var messages: MessagesStore!
19+
var messages: MessageStore { Dependencies.shared.messages }
2120

22-
init() {
21+
private init() {
2322
Task {
24-
channels = try await fetchChannels()
23+
channels = await fetchChannels()
2524

2625
let channel = await supabase.realtimeV2.channel("public:channels")
2726

@@ -44,6 +43,20 @@ final class ChannelsViewModel: ChannelsStore {
4443
}
4544
}
4645

46+
func addChannel(_ name: String) async {
47+
do {
48+
let userId = try await supabase.auth.session.user.id
49+
let channel = AddChannel(slug: name, createdBy: userId)
50+
try await supabase.database
51+
.from("channels")
52+
.insert(channel)
53+
.execute()
54+
} catch {
55+
dump(error)
56+
toast = .init(status: .error, title: "Error", description: error.localizedDescription)
57+
}
58+
}
59+
4760
func fetchChannel(id: Channel.ID) async throws -> Channel {
4861
if let channel = channels.first(where: { $0.id == id }) {
4962
return channel
@@ -65,6 +78,7 @@ final class ChannelsViewModel: ChannelsStore {
6578
channels.append(channel)
6679
} catch {
6780
dump(error)
81+
toast = .init(status: .error, title: "Error", description: error.localizedDescription)
6882
}
6983
}
7084

@@ -74,7 +88,13 @@ final class ChannelsViewModel: ChannelsStore {
7488
messages.removeMessages(for: id)
7589
}
7690

77-
private func fetchChannels() async throws -> [Channel] {
78-
try await supabase.database.from("channels").select().execute().value
91+
private func fetchChannels() async -> [Channel] {
92+
do {
93+
return try await supabase.database.from("channels").select().execute().value
94+
} catch {
95+
dump(error)
96+
toast = .init(status: .error, title: "Error", description: error.localizedDescription)
97+
return []
98+
}
7999
}
80100
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//
2+
// Dependencies.swift
3+
// SlackClone
4+
//
5+
// Created by Guilherme Souza on 04/01/24.
6+
//
7+
8+
import Foundation
9+
import Supabase
10+
11+
class Dependencies {
12+
static let shared = Dependencies()
13+
14+
let channel = ChannelStore.shared
15+
let users = UserStore.shared
16+
let messages = MessageStore.shared
17+
}
18+
19+
struct User: Codable, Identifiable, Hashable {
20+
var id: UUID
21+
var username: String
22+
}
23+
24+
struct AddChannel: Encodable {
25+
var slug: String
26+
var createdBy: UUID
27+
}
28+
29+
struct Channel: Identifiable, Codable, Hashable {
30+
var id: Int
31+
var slug: String
32+
var insertedAt: Date
33+
}
34+
35+
struct Message: Identifiable, Codable, Hashable {
36+
var id: Int
37+
var insertedAt: Date
38+
var message: String
39+
var user: User
40+
var channel: Channel
41+
}
42+
43+
struct NewMessage: Codable {
44+
var message: String
45+
var userId: UUID
46+
let channelId: Int
47+
}
48+
49+
struct UserPresence: Codable, Hashable {
50+
var userId: UUID
51+
var onlineAt: Date
52+
}

Examples/SlackClone/Logger.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//
2+
// Logger.swift
3+
// SlackClone
4+
//
5+
// Created by Guilherme Souza on 23/01/24.
6+
//
7+
8+
import Foundation
9+
import OSLog
10+
import Supabase
11+
12+
extension Logger {
13+
static let main = Self(subsystem: "com.supabase.SlackClone", category: "app")
14+
}
15+
16+
final class SupabaseLoggerImpl: SupabaseLogger, @unchecked Sendable {
17+
private let lock = NSLock()
18+
private var loggers: [String: Logger] = [:]
19+
20+
func log(message: SupabaseLogMessage) {
21+
lock.withLock {
22+
if loggers[message.system] == nil {
23+
loggers[message.system] = Logger(
24+
subsystem: "com.supabase.SlackClone.supabase-swift",
25+
category: message.system
26+
)
27+
}
28+
29+
let logger = loggers[message.system]!
30+
31+
switch message.level {
32+
case .debug: logger.debug("\(message)")
33+
case .error: logger.error("\(message)")
34+
case .verbose: logger.info("\(message)")
35+
case .warning: logger.notice("\(message)")
36+
}
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)