Last active
June 21, 2023 09:31
-
-
Save SergeiMeza/80f76f2fea7a26d33a03c87496dc8ce3 to your computer and use it in GitHub Desktop.
Chat App Client (UI) for macOS in SwiftUI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Main | |
@main | |
struct ChatApp_macOSApp: App { | |
var body: some Scene { | |
WindowGroup { | |
Home() | |
} | |
// Hiding Title Bar... | |
.windowStyle(HiddenTitleBarWindowStyle()) | |
} | |
} | |
// Hiding Textfield Focus Ring... | |
extension NSTextField { | |
open override var focusRingType: NSFocusRingType { | |
get { .none } | |
set {} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Models | |
import SwiftUI | |
// Recent Message Model.... | |
struct RecentMessage : Identifiable { | |
var id = UUID().uuidString | |
var lastMessage : String | |
var lastMessageTime : String | |
var pendingMessages : String | |
var userName : String | |
var userImage : String | |
var allMessages: [Message] | |
} | |
var recentMessages : [RecentMessage] = [ | |
RecentMessage( lastMessage: "Apple Tech", lastMessageTime: "15:00", pendingMessages: "9", userName: "Jenna Ezarik", userImage: "p0", allMessages: Eachmsg.shuffled()), | |
RecentMessage(lastMessage: "New Album Is Going To Be Released!!!!", lastMessageTime: "14:32", pendingMessages: "2", userName: "Taylor", userImage: "p1", allMessages: Eachmsg.shuffled()) | |
,RecentMessage( lastMessage: "Hi this is Steve Rogers !!!", lastMessageTime: "14:35", pendingMessages: "2", userName: "Steve", userImage: "p2", allMessages: Eachmsg.shuffled()) | |
,RecentMessage( lastMessage: "New Tutorial !!!", lastMessageTime: "14:39", pendingMessages: "1", userName: "Sergei Meza", userImage: "p3", allMessages: Eachmsg.shuffled()) | |
,RecentMessage(lastMessage: "New SwiftUI API Is Released!!!!", lastMessageTime: "14:50", pendingMessages: "", userName: "SwiftUI", userImage: "p4", allMessages: Eachmsg.shuffled()), | |
RecentMessage( lastMessage: "Founder Of Microsoft !!!", lastMessageTime: "14:50", pendingMessages: "", userName: "Bill Gates", userImage: "p5", allMessages: Eachmsg.shuffled()), | |
RecentMessage( lastMessage: "Founder Of Amazon", lastMessageTime: "14:39", pendingMessages: "1", userName: "Jeff", userImage: "p6", allMessages: Eachmsg.shuffled()), | |
RecentMessage(lastMessage: "Released New iPhone 11!!!", lastMessageTime: "14:32", pendingMessages: "2", userName: "Tim Cook", userImage: "p7", allMessages: Eachmsg.shuffled()) | |
] | |
// Message Model... | |
struct Message : Identifiable,Equatable { | |
var id = UUID().uuidString | |
var message : String | |
var myMessage : Bool | |
} | |
var Eachmsg = [ | |
Message(message: "New Album Is Going To Be Released!!!!", myMessage: false), | |
Message(message: "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment!!!", myMessage: false), | |
Message(message: "Amazon.in: Online Shopping India - Buy mobiles, laptops, cameras, books, watches, apparel, shoes and e-Gift Cards.", myMessage: false), | |
Message(message: "SwiftUI is an innovative, exceptionally simple way to build user interfaces across all Apple platforms with the power of Swift. Build user interfaces for any Apple device using just one set of tools and APIs.", myMessage: true), | |
Message(message: "At Microsoft our mission and values are to help people and businesses throughout the world realize their full potential.!!!!", myMessage: false), | |
Message(message: "Firebase is Google's mobile platform that helps you quickly develop high-quality apps and grow your business.", myMessage: true), | |
Message(message: "SergeiMeza - SwiftUI Tutorials - Easier Way To Learn SwiftUI With Downloadble Source Code.!!!!", myMessage: true) | |
] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// View Models | |
import SwiftUI | |
// Home View Model... | |
class HomeViewModel: ObservableObject { | |
@Published var selectedTab = "All Chats" | |
@Published var messages: [RecentMessage] = recentMessages | |
// Selected Recent Tab... | |
@Published var selectedRecentMessage: String? = recentMessages.first?.id | |
// Search ... | |
@Published var search = "" | |
// Message ... | |
@Published var message = "" | |
// Expanded Left Side View... | |
@Published var isLeftExpanded = true | |
// Expanded Right Side View... | |
@Published var isRightExpanded = true | |
// Picker Expanded Tab... | |
@Published var pickedTab = "Media" | |
// Send Message... | |
func sendMessage(user: RecentMessage) { | |
if message != "", let index = messages.firstIndex(where: { currentUser -> Bool in | |
return currentUser.id == user.id | |
}) { | |
messages[index].allMessages.append(Message(message: message, myMessage: true)) | |
message = "" | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// View Modifiers | |
import SwiftUI | |
struct TabImageModifier: ViewModifier { | |
var isSelected: Bool | |
func body(content: Content) -> some View { | |
return content | |
.font(.system(size: 16, weight: .semibold)) | |
.foregroundColor(isSelected == true ? .primary: .gray) | |
} | |
} | |
struct TabTitleModifier: ViewModifier { | |
var isSelected: Bool | |
func body(content: Content) -> some View { | |
return content | |
.font(.system(size: 11, weight: .semibold)) | |
.foregroundColor(isSelected == true ? .primary : .gray) | |
} | |
} | |
struct TabButtonModifier: ViewModifier { | |
var isSelected: Bool | |
func body(content: Content) -> some View { | |
return content | |
.padding(.vertical, 8) | |
.frame(width: 70) | |
.contentShape(Rectangle()) | |
.background(Color.primary.opacity(isSelected == true ? 0.15 : 0)) | |
.cornerRadius(10) | |
} | |
} | |
extension Image { | |
func tabImage(isSelected: Bool) -> some View { | |
return self.modifier(TabImageModifier(isSelected: isSelected)) | |
} | |
} | |
extension Text { | |
func tabTitle(isSelected: Bool) -> some View { | |
return self.modifier(TabTitleModifier(isSelected: isSelected)) | |
} | |
} | |
extension View { | |
func tabButton(isSelected: Bool) -> some View { | |
return self.modifier(TabButtonModifier(isSelected: isSelected)) | |
} | |
} | |
struct IconButtonModifier: ViewModifier { | |
func body(content: Content) -> some View { | |
return content | |
.font(.title2) | |
} | |
} | |
struct TextBoxModifier: ViewModifier { | |
func body(content: Content) -> some View { | |
return content | |
.padding(.vertical, 8) | |
.padding(.horizontal) | |
.background(Color.primary.opacity(0.1)) | |
.cornerRadius(10) | |
} | |
} | |
extension View { | |
func iconButton() -> some View { | |
return self.modifier(IconButtonModifier()) | |
} | |
func searchBar() -> some View { | |
return self.modifier(TextBoxModifier()) | |
} | |
func inputBar() -> some View { | |
return self.modifier(TextBoxModifier()) | |
} | |
} | |
struct ProfileImageModifier: ViewModifier { | |
var size: CGFloat = 40 | |
func body(content: Content) -> some View { | |
return content | |
.frame(width: size, height: size) | |
.background(Color(.systemGray).opacity(0.4)) | |
.clipShape(Circle()) | |
} | |
} | |
extension Image { | |
func profileImage(size: CGFloat = 40) -> some View { | |
return self | |
.resizable() | |
.aspectRatio(contentMode: .fill) | |
.modifier(ProfileImageModifier()) | |
} | |
} | |
struct IncomingMessageBubbleModfier: ViewModifier { | |
func body(content: Content) -> some View { | |
return content | |
.foregroundColor(.primary) | |
.padding(10) | |
.background(Color.primary.opacity(0.1)) | |
.clipShape(MessageBubble()) | |
} | |
} | |
// Message Bubble... | |
struct MessageBubble: Shape { | |
func path(in rect: CGRect) -> Path { | |
return Path { path in | |
let pt1 = CGPoint(x: 0, y: 0) | |
let pt2 = CGPoint(x: rect.width, y: 0) | |
let pt3 = CGPoint(x: rect.width, y: rect.height) | |
let pt4 = CGPoint(x: 0, y: rect.height) | |
path.move(to: pt4) | |
path.addArc(tangent1End: pt4, tangent2End: pt1, radius: 15) | |
path.addArc(tangent1End: pt1, tangent2End: pt2, radius: 15) | |
path.addArc(tangent1End: pt2, tangent2End: pt3, radius: 15) | |
path.addArc(tangent1End: pt3, tangent2End: pt4, radius: 15) | |
} | |
} | |
} | |
struct OutgoingMessageBubbleModfier: ViewModifier { | |
func body(content: Content) -> some View { | |
return content | |
.foregroundColor(.white) | |
.padding(10) | |
.background(Color.blue) | |
.cornerRadius(15) | |
} | |
} | |
extension View { | |
func messageBubble(incoming: Bool) -> some View { | |
if incoming == true { | |
return AnyView(self.modifier(IncomingMessageBubbleModfier())) | |
} else { | |
return AnyView(self.modifier(OutgoingMessageBubbleModfier())) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Views | |
import SwiftUI | |
var screen = NSScreen.main!.visibleFrame | |
struct Home: View { | |
@StateObject var homeData = HomeViewModel() | |
var body: some View { | |
HStack(spacing: 0) { | |
// App Tab Bar... | |
AppTabBar() | |
.padding() | |
.padding(.top, 35) | |
.background(BlurView()) | |
Divider() | |
// Tab Content... | |
TabContent() | |
.frame(maxWidth: .infinity, maxHeight: .infinity) | |
} | |
.ignoresSafeArea(.all, edges: .all) | |
.frame(width: screen.width / 1.1, height: screen.height - 60) | |
// Inyecting state object as environment dependency to child views... | |
.environmentObject(homeData) | |
} | |
} | |
// App Tab Bar ... | |
struct AppTabBar: View { | |
@EnvironmentObject var homeData: HomeViewModel | |
var body: some View { | |
VStack { | |
TabButton(image: "message", title: "All Chats", selectedTab: $homeData.selectedTab) | |
TabButton(image: "person", title: "Personal", selectedTab: $homeData.selectedTab) | |
TabButton(image: "bubble.middle.bottom", title: "Bots", selectedTab: $homeData.selectedTab) | |
TabButton(image: "slider.horizontal.3", title: "Edit", selectedTab: $homeData.selectedTab) | |
Spacer() | |
TabButton(image: "gear", title: "Settings", selectedTab: $homeData.selectedTab) | |
} | |
} | |
} | |
// Tab Content .... | |
struct TabContent: View { | |
@EnvironmentObject var homeData: HomeViewModel | |
var body: some View { | |
ZStack { | |
switch homeData.selectedTab { | |
case "All Chats": NavigationView { | |
AllChatsView() | |
} | |
case "Personal": Text("Personal") | |
case "Bots": Text("Bots") | |
case "Edit": Text("Edit") | |
case "Settings": Text("Settings") | |
default: Text("") | |
} | |
} | |
} | |
} | |
struct TabButton: View { | |
var image: String | |
var title: String | |
@Binding var selectedTab: String | |
var isSelected: Bool { | |
selectedTab == title | |
} | |
var body: some View { | |
Button(action: { | |
withAnimation {selectedTab = title } | |
}, label: { | |
VStack(spacing: 7) { | |
Image(systemName: image) | |
.tabImage(isSelected: isSelected) | |
Text(title) | |
.tabTitle(isSelected: isSelected) | |
} | |
.tabButton(isSelected: isSelected) | |
}) | |
.buttonStyle(PlainButtonStyle()) | |
} | |
} | |
struct BlurView: NSViewRepresentable { | |
func makeNSView(context: Context) -> NSVisualEffectView { | |
let view = NSVisualEffectView() | |
view.blendingMode = .behindWindow | |
return view | |
} | |
func updateNSView(_ nsView: NSVisualEffectView, context: Context) { | |
} | |
} | |
struct AllChatsView: View { | |
@Environment(\.colorScheme) var colorScheme | |
@EnvironmentObject var homeData: HomeViewModel | |
var body: some View { | |
// Side Tab View... | |
VStack { | |
HStack { | |
Spacer() | |
Button(action: {}, label: { | |
Image(systemName: "plus") | |
.iconButton() | |
}) | |
.buttonStyle(PlainButtonStyle()) | |
} | |
.padding(.horizontal) | |
HStack { | |
Image(systemName: "magnifyingglass") | |
.foregroundColor(.gray) | |
TextField("Search", text: $homeData.search) | |
.textFieldStyle(PlainTextFieldStyle()) | |
} | |
.searchBar() | |
.padding(10) | |
List(selection: $homeData.selectedRecentMessage) { | |
ForEach(homeData.messages) { message in | |
// Message View... | |
NavigationLink( | |
destination: DetailView(user: message), | |
label: { | |
RecentMessageCardView(recentMessage: message) | |
}) | |
} | |
} | |
.listStyle(SidebarListStyle()) | |
} | |
} | |
} | |
struct RecentMessageCardView: View { | |
var recentMessage: RecentMessage | |
var body: some View { | |
HStack { | |
Image(recentMessage.userImage) | |
.profileImage(size: 40) | |
VStack(spacing: 4) { | |
HStack { | |
VStack(alignment: .leading, spacing: 4, content: { | |
Text(recentMessage.userName) | |
.fontWeight(.bold) | |
Text(recentMessage.lastMessage) | |
.font(.caption) | |
.fontWeight(recentMessage.pendingMessages == "" ? .regular : .semibold) | |
}) | |
Spacer(minLength: 10) | |
VStack { | |
Text(recentMessage.lastMessageTime) | |
.font(.caption) | |
Text(recentMessage.pendingMessages) | |
.font(.caption2) | |
.padding(5) | |
.foregroundColor(.white) | |
.background(Color.blue) | |
.clipShape(Circle()) | |
.opacity(recentMessage.pendingMessages == "" ? 0 : 1) | |
} | |
} | |
} | |
} | |
} | |
} | |
struct DetailView: View { | |
@EnvironmentObject var homeData: HomeViewModel | |
var user: RecentMessage | |
var body: some View { | |
HStack { | |
VStack { | |
HStack(spacing: 15) { | |
Button(action: { | |
homeData.isLeftExpanded.toggle() | |
NSApp.keyWindow?.firstResponder?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil) | |
}, label: { | |
Image(systemName: "sidebar.left") | |
.font(.title2) | |
.foregroundColor(homeData.isLeftExpanded ? .blue : .primary) | |
}) | |
.buttonStyle(PlainButtonStyle()) | |
Text(user.userName) | |
.font(.title2) | |
Spacer() | |
Button(action: {}, label: { | |
Image(systemName: "magnifyingglass") | |
.iconButton() | |
}) | |
.buttonStyle(PlainButtonStyle()) | |
Button(action: { | |
withAnimation { | |
homeData.isRightExpanded.toggle() | |
} | |
}, label: { | |
Image(systemName: "sidebar.right") | |
.iconButton() | |
.foregroundColor(homeData.isRightExpanded ? .blue : .primary) | |
}) | |
.buttonStyle(PlainButtonStyle()) | |
} | |
.padding() | |
// Message View | |
MessageView(user: user) | |
HStack(spacing: 15) { | |
Button(action: {}, label: { | |
Image(systemName: "paperplane") | |
.iconButton() | |
}) | |
.buttonStyle(PlainButtonStyle()) | |
HStack { | |
TextField( | |
"Enter Message", | |
text: $homeData.message, | |
onCommit: { | |
homeData.sendMessage(user: user) | |
}) | |
.textFieldStyle(PlainTextFieldStyle()) | |
} | |
.inputBar() | |
Button(action: {}, label: { | |
Image(systemName: "face.smiling.fill") | |
.iconButton() | |
}) | |
.buttonStyle(PlainButtonStyle()) | |
Button(action: {}, label: { | |
Image(systemName: "mic") | |
.iconButton() | |
}) | |
.buttonStyle(PlainButtonStyle()) | |
} | |
.padding([.horizontal, .bottom]) | |
} | |
.frame(minWidth: 700) | |
ExpandedView(user: user) | |
.background(BlurView()) | |
.frame(width: homeData.isRightExpanded ? nil : 0) | |
.opacity(homeData.isRightExpanded ? 1 : 0) | |
} | |
.ignoresSafeArea(.all, edges: .all) | |
} | |
} | |
// Message View... | |
struct MessageView: View { | |
@EnvironmentObject var homeData: HomeViewModel | |
var user: RecentMessage | |
var body: some View { | |
GeometryReader { reader in | |
ScrollView { | |
ScrollViewReader { proxy in | |
VStack(spacing: 18) { | |
ForEach(user.allMessages) { message in | |
// Message Card View... | |
MessageCardView( | |
message: message, | |
user: user, | |
width: reader.frame(in: .global).width) | |
.tag(message.id) | |
} | |
.onAppear(perform: { | |
// Showing Last Message | |
if let lastId = user.allMessages.last?.id { | |
proxy.scrollTo(lastId, anchor: .bottom) | |
} | |
}) | |
.onChange(of: user.allMessages, perform: { value in | |
// Same For WHen New Message Appended | |
if let lastId = user.allMessages.last?.id { | |
proxy.scrollTo(lastId, anchor: .bottom) | |
} | |
}) | |
} | |
.padding(.bottom, 30) | |
} | |
} | |
} | |
} | |
} | |
// Message Card View... | |
struct MessageCardView: View { | |
@EnvironmentObject var homeData: HomeViewModel | |
var message: Message | |
var user: RecentMessage | |
var width: CGFloat | |
var body: some View { | |
HStack(spacing: 10) { | |
if message.myMessage { | |
Spacer() | |
Text(message.message) | |
.messageBubble(incoming: false) | |
// MaxWidth... | |
.frame(minWidth: 30, maxWidth: width / 2, alignment: .trailing) | |
} else { | |
Image(user.userImage) | |
.profileImage(size: 35) | |
.offset(y: 20) | |
Text(message.message) | |
.messageBubble(incoming: true) | |
// MaxWidth... | |
.frame(minWidth: 30, maxWidth: width / 2, alignment: .leading) | |
Spacer() | |
} | |
} | |
.padding(.horizontal) | |
} | |
} | |
// Expanded View... | |
struct ExpandedView: View { | |
@EnvironmentObject var homeData: HomeViewModel | |
var user: RecentMessage | |
var body: some View { | |
HStack(spacing: 0) { | |
Divider() | |
VStack(spacing: 25) { | |
Image(user.userImage) | |
.profileImage(size: 90) | |
.padding(.top, 35) | |
Text(user.userName) | |
.font(.title) | |
.fontWeight(.bold) | |
ProfileActionsSection() | |
ProfileAttachmentsSection() | |
} | |
.padding(.horizontal) | |
.frame(maxWidth: 300) | |
} | |
} | |
} | |
struct ProfileActionsSection: View { | |
var body: some View { | |
HStack { | |
Button(action: {}, label: { | |
VStack { | |
Image(systemName: "bell.slash") | |
.iconButton() | |
Text("Mute") | |
} | |
.contentShape(Rectangle()) | |
}) | |
.buttonStyle(PlainButtonStyle()) | |
Spacer() | |
Button(action: {}, label: { | |
VStack { | |
Image(systemName: "hand.raised.fill") | |
.iconButton() | |
Text("Block") | |
} | |
.contentShape(Rectangle()) | |
}) | |
.buttonStyle(PlainButtonStyle()) | |
Spacer() | |
Button(action: {}, label: { | |
VStack { | |
Image(systemName: "exclamationmark.triangle") | |
.iconButton() | |
Text("Report") | |
} | |
.contentShape(Rectangle()) | |
}) | |
.buttonStyle(PlainButtonStyle()) | |
} | |
.foregroundColor(.gray) | |
} | |
} | |
struct ProfileAttachmentsSection: View { | |
@EnvironmentObject var homeData: HomeViewModel | |
var body: some View { | |
Group { | |
Picker(selection: $homeData.pickedTab, label: Text("Picker"), content: { | |
Text("Media").tag("Media") | |
Text("Links").tag("Links") | |
Text("Audio").tag("Audio") | |
Text("Files").tag("Files") | |
}) | |
.pickerStyle(SegmentedPickerStyle()) | |
.labelsHidden() | |
.padding(.vertical) | |
ScrollView { | |
if homeData.pickedTab == "Media" { | |
// Grid of Photos... | |
LazyVGrid( | |
columns: Array( | |
repeating: GridItem( | |
.flexible(), | |
spacing: 10), | |
count: 3), | |
spacing: 10, | |
content: { | |
ForEach(1...8, id: \.self) { index in | |
Image("media\(index)") | |
.resizable() | |
.aspectRatio(contentMode: .fill) | |
// Horizontal padding = 30 | |
// Spacing = 30 | |
// Width = (300 - 60)/3 = 80 | |
.frame(width: 80, height: 80) | |
.cornerRadius(3) | |
} | |
}) | |
} else { | |
Text("No \(homeData.pickedTab)") | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment