Skip to content

Instantly share code, notes, and snippets.

@SergeiMeza
Last active June 21, 2023 09:31
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SergeiMeza/80f76f2fea7a26d33a03c87496dc8ce3 to your computer and use it in GitHub Desktop.
Save SergeiMeza/80f76f2fea7a26d33a03c87496dc8ce3 to your computer and use it in GitHub Desktop.
Chat App Client (UI) for macOS in SwiftUI
// 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 {}
}
}
// 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)
]
// 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 = ""
}
}
}
// 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()))
}
}
}
// 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