├── readme └── presentation.gif ├── SwiftUI-WeChatDemo ├── Resources │ ├── Color.xcassets │ │ ├── Contents.json │ │ ├── Me │ │ │ ├── Contents.json │ │ │ ├── FavoritesGradient │ │ │ │ ├── Contents.json │ │ │ │ ├── favorites.blue.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── favorites.red.colorset │ │ │ │ │ └── Contents.json │ │ │ │ └── favorites.yellow.colorset │ │ │ │ │ └── Contents.json │ │ │ ├── CardsBlue.colorset │ │ │ │ └── Contents.json │ │ │ ├── PayGreen.colorset │ │ │ │ └── Contents.json │ │ │ ├── SettingsBlue.colorset │ │ │ │ └── Contents.json │ │ │ ├── MeMomentsBlue.colorset │ │ │ │ └── Contents.json │ │ │ └── StickerYellow.colorset │ │ │ │ └── Contents.json │ │ ├── Discover │ │ │ ├── Contents.json │ │ │ ├── MomentsGradient │ │ │ │ ├── Contents.json │ │ │ │ ├── moments.blue.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── moments.green.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── moments.red.colorset │ │ │ │ │ └── Contents.json │ │ │ │ └── moments.yellow.colorset │ │ │ │ │ └── Contents.json │ │ │ ├── LiveRed.colorset │ │ │ │ └── Contents.json │ │ │ ├── NearbyBlue.colorset │ │ │ │ └── Contents.json │ │ │ ├── ScanBlue.colorset │ │ │ │ └── Contents.json │ │ │ ├── SearchRed.colorset │ │ │ │ └── Contents.json │ │ │ ├── ShakeBlue.colorset │ │ │ │ └── Contents.json │ │ │ ├── ChannelsOrange.colorset │ │ │ │ └── Contents.json │ │ │ ├── TopStoriesYellow.colorset │ │ │ │ └── Contents.json │ │ │ └── MiniProgramsPurple.colorset │ │ │ │ └── Contents.json │ │ ├── ChatGray.colorset │ │ │ └── Contents.json │ │ ├── ChatGreen.colorset │ │ │ └── Contents.json │ │ ├── LightGray.colorset │ │ │ └── Contents.json │ │ ├── SearchGray.colorset │ │ │ └── Contents.json │ │ ├── TabbarGray.colorset │ │ │ └── Contents.json │ │ ├── TopBarGray.colorset │ │ │ └── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── BackgroudGray.colorset │ │ │ └── Contents.json │ │ └── SearchBarBackgroundGray.colorset │ │ │ └── Contents.json │ ├── Image.xcassets │ │ ├── Contents.json │ │ ├── Me │ │ │ ├── Contents.json │ │ │ ├── myprofile.qrcode.imageset │ │ │ │ ├── IMG_3884.jpeg │ │ │ │ ├── IMG_3899.jpeg │ │ │ │ ├── IMG_3884-1.jpeg │ │ │ │ └── Contents.json │ │ │ ├── me.cards.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── uf_pay.svg │ │ │ ├── me.channels.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── play.svg │ │ │ ├── me.favorites.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── box-f.svg │ │ │ ├── me.moments.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── image.svg │ │ │ ├── me.pay.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── wechat-pay.svg │ │ │ ├── me.settings.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── Gear-2.svg │ │ │ ├── me.sticker.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── smile.svg │ │ │ └── myprofile.qrcode.icon.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── qr-code.svg │ │ ├── avatar │ │ │ ├── Contents.json │ │ │ ├── avatar_me.imageset │ │ │ │ ├── IMG_8372_feet.jpg │ │ │ │ └── Contents.json │ │ │ ├── avatar01.imageset │ │ │ │ ├── 5b250effec2628ba9620e4286f3bc67e.jpg │ │ │ │ └── Contents.json │ │ │ ├── avatar02.imageset │ │ │ │ ├── 8bab2fc8ec9acd8a18d4e199f924dc69.jpg │ │ │ │ └── Contents.json │ │ │ ├── avatar03.imageset │ │ │ │ ├── 4804b02102e1b2b7e3ac9ad8356c3b25.jpg │ │ │ │ └── Contents.json │ │ │ ├── avatar04.imageset │ │ │ │ ├── aa02db97618aab351516b2b4cbf2abdb.jpg │ │ │ │ └── Contents.json │ │ │ ├── avatar05.imageset │ │ │ │ ├── edcf8932a5c7cbb1beb829a0e0f5a584.jpg │ │ │ │ └── Contents.json │ │ │ ├── avatar06.imageset │ │ │ │ ├── 8eb4993993b20c6c10d0aa025cf2be92.jpg │ │ │ │ └── Contents.json │ │ │ ├── avatar07.imageset │ │ │ │ ├── 5aa8dfe1a44900201c9b609760b0919d.jpg │ │ │ │ └── Contents.json │ │ │ ├── avatar09.imageset │ │ │ │ ├── b92af60f3777dbcd468dce8a412e2ee3.jpg │ │ │ │ └── Contents.json │ │ │ ├── avatar10.imageset │ │ │ │ ├── a852a0b611108e9fcf588a536212b389.jpg │ │ │ │ └── Contents.json │ │ │ └── avatar08.imageset │ │ │ │ ├── v2-c6af5dfc30aedca77e846825b021ee7f_1440w.png │ │ │ │ └── Contents.json │ │ ├── discover │ │ │ ├── Contents.json │ │ │ ├── discover.moments.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── shutter.svg │ │ │ ├── discover.scan.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── hand-rock.svg │ │ │ ├── discover.shake.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── shake-it.svg │ │ │ ├── discover.channels.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── butterfly_f.svg │ │ │ ├── discover.live.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── round-album-24px.svg │ │ │ ├── discover.search.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── ic_fireworks.svg │ │ │ ├── discover.topStories.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── md-flower.svg │ │ │ ├── discover.miniPrograms.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── science-dna.svg │ │ │ └── discover.nearby.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── person_circle_fill.svg │ │ ├── AppIcon.appiconset │ │ │ ├── icon-29.png │ │ │ ├── icon-40.png │ │ │ ├── icon-76.png │ │ │ ├── icon-1024.png │ │ │ ├── icon-20-ipad.png │ │ │ ├── icon-20@2x.png │ │ │ ├── icon-20@3x.png │ │ │ ├── icon-29-ipad.png │ │ │ ├── icon-29@2x.png │ │ │ ├── icon-29@3x.png │ │ │ ├── icon-40@2x.png │ │ │ ├── icon-40@3x.png │ │ │ ├── icon-60@2x.png │ │ │ ├── icon-60@3x.png │ │ │ ├── icon-76@2x.png │ │ │ ├── icon-83.5@2x.png │ │ │ ├── icon-20@2x-ipad.png │ │ │ ├── icon-29@2x-ipad.png │ │ │ └── Contents.json │ │ └── Splash.imageset │ │ │ ├── wp5576810@3x.png │ │ │ └── Contents.json │ ├── en.lproj │ │ ├── Contact.strings │ │ ├── LastMessage.strings │ │ └── Localizable.strings │ └── zh-Hans.lproj │ │ ├── Contact.strings │ │ ├── LastMessage.strings │ │ └── Localizable.strings ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── en.lproj │ └── InfoPlist.strings ├── zh-Hans.lproj │ └── InfoPlist.strings ├── Data │ ├── MyProfile.swift │ ├── Language.swift │ ├── ContactList.swift │ └── ChatList.swift ├── WeChatDemoApp.swift ├── UI │ ├── Common │ │ ├── extension │ │ │ └── View.swift │ │ ├── SearchBarView.swift │ │ └── ItemBarView.swift │ ├── Chats │ │ ├── Detail │ │ │ ├── BindingModel │ │ │ │ ├── ChatMessage.swift │ │ │ │ └── KeyboardVisibilityReporter.swift │ │ │ ├── ChatInputView.swift │ │ │ ├── ChatDetailView.swift │ │ │ ├── ChatFlowView.swift │ │ │ ├── ViewModel │ │ │ │ └── ChatDetailViewModel.swift │ │ │ └── ChatMessageView.swift │ │ ├── ChatsView.swift │ │ └── ChatItemView.swift │ ├── Me │ │ ├── LanguageSwitcher.swift │ │ ├── QrCodeView.swift │ │ ├── MeView.swift │ │ ├── MyProfileView.swift │ │ └── Data │ │ │ └── MeItems.swift │ ├── Contacts │ │ ├── ContactItemView.swift │ │ └── ContactsView.swift │ ├── Discover │ │ ├── DiscoverView.swift │ │ └── Data │ │ │ └── DiscoverItems.swift │ └── TabContainer.swift ├── Info.plist └── Generated │ ├── stringsExtra.swift │ ├── strings.swift │ └── assets.swift ├── SwiftUI-WeChatDemo.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcuserdata │ └── wavky.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── xcshareddata │ └── xcschemes │ └── SwiftUI-WeChatDemo.xcscheme ├── swiftgen.yml ├── README.md ├── .gitignore ├── swiftui-strings-template.stencil ├── swiftui-assets-template.stencil └── LICENSE /readme/presentation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/readme/presentation.gif -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Me/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Discover/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/discover/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Me/FavoritesGradient/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Discover/MomentsGradient/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-29.png -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-40.png -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-76.png -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-1024.png -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Splash.imageset/wp5576810@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/Splash.imageset/wp5576810@3x.png -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-20-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-20-ipad.png -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-20@2x.png -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-20@3x.png -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-29-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-29-ipad.png -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-29@2x.png -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-29@3x.png -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-40@2x.png -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-40@3x.png -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-60@2x.png -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-60@3x.png -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-76@2x.png -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-83.5@2x.png -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | InfoPlist.strings 3 | SwiftUI-WeChatDemo 4 | 5 | Created by Wavky Huang on 2021/07/14. 6 | 7 | */ 8 | 9 | CFBundleDisplayName = "WeChatDemo"; 10 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/zh-Hans.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | InfoPlist.strings 3 | SwiftUI-WeChatDemo 4 | 5 | Created by Wavky Huang on 2021/07/14. 6 | 7 | */ 8 | 9 | CFBundleDisplayName = "微信 Demo"; 10 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/myprofile.qrcode.imageset/IMG_3884.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/myprofile.qrcode.imageset/IMG_3884.jpeg -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/myprofile.qrcode.imageset/IMG_3899.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/myprofile.qrcode.imageset/IMG_3899.jpeg -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/myprofile.qrcode.imageset/IMG_3884-1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/myprofile.qrcode.imageset/IMG_3884-1.jpeg -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar_me.imageset/IMG_8372_feet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar_me.imageset/IMG_8372_feet.jpg -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar01.imageset/5b250effec2628ba9620e4286f3bc67e.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar01.imageset/5b250effec2628ba9620e4286f3bc67e.jpg -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar02.imageset/8bab2fc8ec9acd8a18d4e199f924dc69.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar02.imageset/8bab2fc8ec9acd8a18d4e199f924dc69.jpg -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar03.imageset/4804b02102e1b2b7e3ac9ad8356c3b25.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar03.imageset/4804b02102e1b2b7e3ac9ad8356c3b25.jpg -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar04.imageset/aa02db97618aab351516b2b4cbf2abdb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar04.imageset/aa02db97618aab351516b2b4cbf2abdb.jpg -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar05.imageset/edcf8932a5c7cbb1beb829a0e0f5a584.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar05.imageset/edcf8932a5c7cbb1beb829a0e0f5a584.jpg -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar06.imageset/8eb4993993b20c6c10d0aa025cf2be92.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar06.imageset/8eb4993993b20c6c10d0aa025cf2be92.jpg -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar07.imageset/5aa8dfe1a44900201c9b609760b0919d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar07.imageset/5aa8dfe1a44900201c9b609760b0919d.jpg -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar09.imageset/b92af60f3777dbcd468dce8a412e2ee3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar09.imageset/b92af60f3777dbcd468dce8a412e2ee3.jpg -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar10.imageset/a852a0b611108e9fcf588a536212b389.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar10.imageset/a852a0b611108e9fcf588a536212b389.jpg -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar08.imageset/v2-c6af5dfc30aedca77e846825b021ee7f_1440w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wavky/SwiftUI-WeChatDemo/HEAD/SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar08.imageset/v2-c6af5dfc30aedca77e846825b021ee7f_1440w.png -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/me.cards.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "uf_pay.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/me.channels.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "play.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/me.favorites.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "box-f.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/me.moments.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "image.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/me.pay.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "wechat-pay.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/me.settings.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Gear-2.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/me.sticker.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "smile.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Splash.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "wp5576810@3x.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/myprofile.qrcode.icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "qr-code.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/discover/discover.moments.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "shutter.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/discover/discover.scan.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "hand-rock.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/discover/discover.shake.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "shake-it.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/discover/discover.channels.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "butterfly_f.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/discover/discover.live.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "round-album-24px.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/discover/discover.search.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ic_fireworks.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/discover/discover.topStories.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "md-flower.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/discover/discover.miniPrograms.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "science-dna.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/discover/discover.nearby.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "person_circle_fill.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/en.lproj/Contact.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Contact.strings 3 | SwiftUI-WeChatDemo 4 | 5 | Created by Wavky Huang on 2021/07/18. 6 | 7 | */ 8 | 9 | Name.Antony = "Antony"; 10 | Name.Apple = "Apple"; 11 | Name.Bosco = "Bosco"; 12 | Name.Joe = "Joe"; 13 | Name.Microsoft = "Microsoft"; 14 | Name.Muraosa = "Muraosa"; 15 | Name.Ron = "Ron"; 16 | Name.Saha = "Saha"; 17 | Name.Sony = "Sony"; 18 | Name.UncleWang = "UncleWang"; 19 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/zh-Hans.lproj/Contact.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Contact.strings 3 | SwiftUI-WeChatDemo 4 | 5 | Created by Wavky Huang on 2021/07/18. 6 | 7 | */ 8 | 9 | Name.Antony = "安东尼🧑🏻‍💻"; 10 | Name.Apple = "小苹果🍎"; 11 | Name.Bosco = "波💪🏽士"; 12 | Name.Joe = "阿祖"; 13 | Name.Microsoft = "巨硬"; 14 | Name.Muraosa = "💋村长💋"; 15 | Name.Ron = "Ron"; 16 | Name.Saha = "沙哈😈"; 17 | Name.Sony = "索尼大法"; 18 | Name.UncleWang = "你的好邻居王叔叔"; 19 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Data/MyProfile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyProfile.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/17. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct MyProfile { 12 | let profileImage: String = Asset.Image.avatarMe.name 13 | let qrcodeImage: String = Asset.Image.myprofileQrcode.name 14 | let name: LocalizedStringKey = L10n.MyProfile.name.key 15 | let weChatID: String = "Wavky_Huang" 16 | } 17 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar_me.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "IMG_8372_feet.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Data/Language.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Language.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/19. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Language: String, CaseIterable { 11 | case English = "English", Chinese = "中文" 12 | 13 | var code: String { 14 | switch self { 15 | case .English: 16 | return "en" 17 | case .Chinese: 18 | return "zh-Hans" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/ChatGray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.929", 9 | "green" : "0.929", 10 | "red" : "0.929" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/ChatGreen.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.474", 9 | "green" : "0.915", 10 | "red" : "0.658" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/LightGray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.796", 9 | "green" : "0.796", 10 | "red" : "0.796" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/SearchGray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.706", 9 | "green" : "0.706", 10 | "red" : "0.702" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/TabbarGray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF7", 9 | "green" : "0xF6", 10 | "red" : "0xF7" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/TopBarGray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.969", 9 | "green" : "0.969", 10 | "red" : "0.969" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.416", 9 | "green" : "0.745", 10 | "red" : "0.341" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/BackgroudGray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.929", 9 | "green" : "0.929", 10 | "red" : "0.929" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Me/CardsBlue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.914", 9 | "green" : "0.426", 10 | "red" : "0.030" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Me/PayGreen.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.404", 9 | "green" : "0.737", 10 | "red" : "0.224" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Me/SettingsBlue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.898", 9 | "green" : "0.467", 10 | "red" : "0.029" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar01.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "5b250effec2628ba9620e4286f3bc67e.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar02.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "8bab2fc8ec9acd8a18d4e199f924dc69.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar03.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "4804b02102e1b2b7e3ac9ad8356c3b25.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar04.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "aa02db97618aab351516b2b4cbf2abdb.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar05.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "edcf8932a5c7cbb1beb829a0e0f5a584.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar06.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "8eb4993993b20c6c10d0aa025cf2be92.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar07.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "5aa8dfe1a44900201c9b609760b0919d.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar09.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "b92af60f3777dbcd468dce8a412e2ee3.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar10.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "a852a0b611108e9fcf588a536212b389.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Discover/LiveRed.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.263", 9 | "green" : "0.386", 10 | "red" : "0.928" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Discover/NearbyBlue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.886", 9 | "green" : "0.414", 10 | "red" : "0.028" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Discover/ScanBlue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.894", 9 | "green" : "0.410", 10 | "red" : "0.029" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Discover/SearchRed.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.251", 9 | "green" : "0.288", 10 | "red" : "0.897" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Discover/ShakeBlue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.902", 9 | "green" : "0.529", 10 | "red" : "0.279" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Me/MeMomentsBlue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.894", 9 | "green" : "0.451", 10 | "red" : "0.029" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Me/StickerYellow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.747", 10 | "red" : "0.960" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Discover/ChannelsOrange.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.247", 9 | "green" : "0.608", 10 | "red" : "0.924" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Discover/TopStoriesYellow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.266", 9 | "green" : "0.770", 10 | "red" : "0.960" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/SearchBarBackgroundGray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.898", 9 | "green" : "0.898", 10 | "red" : "0.898" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/avatar/avatar08.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "v2-c6af5dfc30aedca77e846825b021ee7f_1440w.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Discover/MiniProgramsPurple.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.898", 9 | "green" : "0.234", 10 | "red" : "0.214" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Me/FavoritesGradient/favorites.blue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.914", 9 | "green" : "0.491", 10 | "red" : "0.086" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Me/FavoritesGradient/favorites.red.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.125", 9 | "green" : "0.167", 10 | "red" : "0.881" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Discover/MomentsGradient/moments.blue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.902", 9 | "green" : "0.300", 10 | "red" : "0.283" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Discover/MomentsGradient/moments.green.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.404", 9 | "green" : "0.741", 10 | "red" : "0.127" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Discover/MomentsGradient/moments.red.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.145", 9 | "green" : "0.202", 10 | "red" : "0.893" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Discover/MomentsGradient/moments.yellow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.263", 9 | "green" : "0.774", 10 | "red" : "0.964" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Color.xcassets/Me/FavoritesGradient/favorites.yellow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.235", 9 | "green" : "0.754", 10 | "red" : "0.960" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/WeChatDemoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUI_WeChatDemoApp.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/14. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct WeChatDemoApp: App { 12 | @AppStorage("AppLanguage") 13 | private var appLanguage: String = Language.English.code 14 | 15 | var body: some Scene { 16 | WindowGroup { 17 | TabContainer() 18 | .environment(\.locale, 19 | .init(identifier: appLanguage)) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/zh-Hans.lproj/LastMessage.strings: -------------------------------------------------------------------------------- 1 | /* 2 | LastMessage.strings 3 | SwiftUI-WeChatDemo 4 | 5 | Created by Wavky Huang on 2021/07/18. 6 | 7 | */ 8 | 9 | Message.Hi = "嗨"; 10 | Message.Hey = "嘿"; 11 | Message.Hello = "哈喽"; 12 | Message.NiceToMeetYou = "很高兴认识你"; 13 | Message.HowAreYou = "怎么是你?"; 14 | Message.Aloha = "一袋米要扛几楼"; 15 | Message.AreYouStillThere = "你还活着吗?"; 16 | Message.ILoveYou = "么么哒😘"; 17 | Message.TheMatrixIsLookingForYou = "妈妈喊你回家吃饭"; 18 | Message.IAmRobot = "我是这条街最靓的仔😎"; 19 | 20 | DateTime.Yesterday = "昨天"; 21 | DateTime.Monday = "星期一"; 22 | DateTime.Sunday = "星期日"; 23 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo.xcodeproj/xcuserdata/wavky.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SwiftUI-WeChatDemo.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | C62D9DDB269DE750003F9923 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/en.lproj/LastMessage.strings: -------------------------------------------------------------------------------- 1 | /* 2 | LastMessage.strings 3 | SwiftUI-WeChatDemo 4 | 5 | Created by Wavky Huang on 2021/07/18. 6 | 7 | */ 8 | 9 | Message.Hi = "Hi"; 10 | Message.Hey = "Hey"; 11 | Message.Hello = "Hello"; 12 | Message.NiceToMeetYou = "Nice to meet you~"; 13 | Message.HowAreYou = "How are you?"; 14 | Message.Aloha = "Aloha"; 15 | Message.AreYouStillThere = "Are you still there?"; 16 | Message.ILoveYou = "I love you!"; 17 | Message.TheMatrixIsLookingForYou = "The Matrix is looking for you."; 18 | Message.IAmRobot = "I am robot."; 19 | 20 | DateTime.Yesterday = "Yesterday"; 21 | DateTime.Monday = "Monday"; 22 | DateTime.Sunday = "Sunday"; 23 | -------------------------------------------------------------------------------- /swiftgen.yml: -------------------------------------------------------------------------------- 1 | input_dir: ${TARGET_NAME}/Resources/ 2 | output_dir: ${TARGET_NAME}/Generated/ 3 | strings: 4 | - inputs: en.lproj/Localizable.strings 5 | outputs: 6 | - templatePath: swiftui-strings-template.stencil 7 | output: strings.swift 8 | - inputs: en.lproj/ 9 | filter: ^((?!InfoPlist|Localizable).)+\.strings$ 10 | outputs: 11 | - templatePath: swiftui-strings-template.stencil 12 | output: stringsExtra.swift 13 | params: 14 | enumName: L10nExtra 15 | withoutSupporter: 16 | xcassets: 17 | inputs: . 18 | filter: .+\.xcassets$ 19 | outputs: 20 | - templatePath: swiftui-assets-template.stencil 21 | output: assets.swift 22 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/UI/Common/extension/View.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/18. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | extension View { 12 | func withBadge(_ show: Bool, radius: CGFloat = 6) -> some View { 13 | if show { 14 | return AnyView(self.overlay(Circle() 15 | .fill(Color.red) 16 | .frame(width: radius * 2, height: radius * 2) 17 | .offset(x: radius, y: -radius), 18 | alignment: .topTrailing)) 19 | } else { 20 | return AnyView(self) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/UI/Chats/Detail/BindingModel/ChatMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatMessage.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/15. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ChatMessage: Hashable { 11 | let side: Side 12 | let text: MessageType 13 | } 14 | 15 | enum MessageType: Hashable { 16 | case String(text: String), 17 | LocalizedString(text: LocalizedString) 18 | } 19 | 20 | enum Side: Hashable { 21 | case Left(profileImage: String), Right 22 | 23 | var profileImage: String { 24 | switch self { 25 | case .Left(let profileImage): 26 | return profileImage 27 | case .Right: 28 | return Asset.Image.avatarMe.name 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/me.pay.imageset/wechat-pay.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/me.cards.imageset/uf_pay.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/UI/Common/SearchBarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchBarView.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/16. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SearchBarView: View { 11 | var body: some View { 12 | HStack { 13 | (Text(Image(systemName: "magnifyingglass"))+Text(" ")+Text(L10n.SearchBar.hint.key)) 14 | .foregroundColor(Asset.Color.searchGray.color) 15 | .padding(8) 16 | .frame(maxWidth: .infinity) 17 | .background(Color.white) 18 | .clipShape(RoundedRectangle(cornerRadius: 4)) 19 | } 20 | .padding(8) 21 | .background(Asset.Color.searchBarBackgroundGray.color) 22 | } 23 | } 24 | 25 | struct SearchBarView_Previews: PreviewProvider { 26 | static var previews: some View { 27 | SearchBarView() 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/discover/discover.live.imageset/round-album-24px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/me.sticker.imageset/smile.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/UI/Me/LanguageSwitcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LanguageSwitcher.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/18. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LanguageSwitcher: View { 11 | private let languages = Language.allCases 12 | 13 | @AppStorage("AppLanguage") 14 | private var appLanguage: String = Language.English.code 15 | 16 | var body: some View { 17 | VStack { 18 | Picker(selection: $appLanguage, label: EmptyView()) { 19 | ForEach(languages, id: \.code) { lang in 20 | Text(lang.rawValue) 21 | } 22 | } 23 | .pickerStyle(SegmentedPickerStyle()) 24 | .padding(.horizontal, 40) 25 | } 26 | } 27 | } 28 | 29 | struct LanguageSwitcher_Previews: PreviewProvider { 30 | static var previews: some View { 31 | LanguageSwitcher() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/zh-Hans.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | SwiftUI-WeChatDemo 4 | 5 | Created by Wavky Huang on 2021/07/14. 6 | 7 | */ 8 | 9 | AppName = "微信"; 10 | 11 | Tabs.Chats = "微信"; 12 | Tabs.Contacts = "通讯录"; 13 | Tabs.Discover = "发现"; 14 | Tabs.Me = "我"; 15 | 16 | SearchBar.Hint = "搜索"; 17 | 18 | Discover.Moments = "朋友圈"; 19 | Discover.Channels = "视频号"; 20 | Discover.Live = "直播"; 21 | Discover.Scan = "扫一扫"; 22 | Discover.Shake = "摇一摇"; 23 | Discover.TopStories = "看一看"; 24 | Discover.Search = "搜一搜"; 25 | Discover.Nearby = "附近"; 26 | Discover.MiniPrograms = "小程序"; 27 | Discover.Live.OnAir = "直播中"; 28 | 29 | Me.Pay = "支付"; 30 | Me.Favorites = "收藏"; 31 | Me.Moments = "朋友圈"; 32 | Me.Channels = "视频号"; 33 | Me.Cards = "卡包"; 34 | Me.Sticker = "表情"; 35 | Me.Settings = "设置"; 36 | 37 | MyProfile.Name = "猫猫腿"; 38 | MyProfile.Status = "状态"; 39 | MyProfile.WeChatID = "微信号: "; 40 | 41 | QrCode.Title = "我的二维码"; 42 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/me.channels.imageset/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/me.favorites.imageset/box-f.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/me.moments.imageset/image.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/discover/discover.topStories.imageset/md-flower.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/discover/discover.scan.imageset/hand-rock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 用 SwiftUI 五天组装一个微信 2 | 3 | ## 效果图 4 | ![](./readme/presentation.gif) 5 | 6 | ## 实装内容 7 | * 4 个 Tab 页面 + 聊天界面,使用纯 SwiftUI 搭建而成 8 | * 应用启动界面 Launch Screen 9 | * 国际化及应用内语言切换功能 10 | * Combine 使用场景(聊天界面简易对话功能) 11 | * 极度简化的 MVVM 架构 12 | * 逻辑部分纯 Swift5 书写,几乎不涉及 OC 类及 UIKit 库 13 | * 不使用第三方框架(除了 SwiftGen 提供便利) 14 | 15 | ## 难点 16 | * 在聊天界面中隐藏 Tabbar(未能实现) 17 | * 聊天界面中信息滚动功能,尤其动画滑动效果难以实现 18 | * 保持键盘焦点,不自动隐藏(有望在 SwiftUI 3 后实现) 19 | * 发现页图标的动态颜色指定(实现起来障碍较大,多番尝试发现无法简单地将 Color 和 一众渐变色类型 存储到单一变量,并且 View 比较难以扩展,语法糖副作用严重) 20 | * SwiftUI 原生视图难以完美复刻微信原生界面(尤其是联系人页),判断是微信使用了大量自定义视图 21 | * Tab 按钮添加红点(无法实现,SwiftUI 原生视图仅接受 Image 和 Text,对 Modifier 的修改直接忽略) 22 | * SwiftUI 中 Combine 的使用(通过 ObservableObject 构造 ViewModel 实现) 23 | * 应用内语言切换(通过 [自定义配置 SwiftGen](https://github.com/wavky/SwiftGenConfigForSwiftUI) 实现) 24 | 25 | ## 坑 26 | * SwiftUI2 中缺乏视图焦点控制方法(该部分将在 SwiftUI3 中提供) 27 | * SwiftUI 中使用 switch 语法时,会导致自动提示失效,但编译能顺利通过,判断是 Xcode 问题 28 | * 目前在书写 SwiftUI 时遇到语法使用错误等情况,偶尔会出现 Xcode 提示未知错误,建议报告 bug(因为逐行书写,基本都能猜出问题所在) 29 | 30 | --- 31 | 32 | ## 彩蛋 33 | 应用内包含微信联系方式,欢迎同行交流(本人是 Android 攻城狮..) 34 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | SwiftUI-WeChatDemo 4 | 5 | Created by Wavky Huang on 2021/07/14. 6 | 7 | */ 8 | 9 | AppName = "WeChat"; 10 | 11 | Tabs.Chats = "Chats"; 12 | Tabs.Contacts = "Contacts"; 13 | Tabs.Discover = "Discover"; 14 | Tabs.Me = "Me"; 15 | 16 | SearchBar.Hint = "Search"; 17 | 18 | Discover.Moments = "Moments"; 19 | Discover.Channels = "Channels"; 20 | Discover.Live = "Live"; 21 | Discover.Scan = "Scan"; 22 | Discover.Shake = "Shake"; 23 | Discover.TopStories = "Top Stories"; 24 | Discover.Search = "Search"; 25 | Discover.Nearby = "Nearby"; 26 | Discover.MiniPrograms = "Mini Programs"; 27 | Discover.Live.OnAir = "Live"; 28 | 29 | Me.Pay = "Pay"; 30 | Me.Favorites = "Favorites"; 31 | Me.Moments = "Moments"; 32 | Me.Channels = "Channels"; 33 | Me.Cards = "Cards & Offers"; 34 | Me.Sticker = "Sticker Gallery"; 35 | Me.Settings = "Settings"; 36 | 37 | MyProfile.Name = "Wavky Huang"; 38 | MyProfile.Status = "Status"; 39 | MyProfile.WeChatID = "WeChat ID: "; 40 | 41 | QrCode.Title = "My QR Code"; 42 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/UI/Chats/Detail/BindingModel/KeyboardVisibilityReporter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardVisibilityReporter.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Copy from https://www.youtube.com/watch?v=D8Eo0c_ciMk 6 | // Created by Wavky Huang on 2021/07/15. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | import SwiftUI 12 | 13 | class KeyboardVisibilityReporter: ObservableObject { 14 | @Published private(set) var isVisible: Bool = false 15 | 16 | private var cancelable: AnyCancellable? 17 | 18 | private let keyboardWillShow = 19 | NotificationCenter.default 20 | .publisher(for: UIResponder.keyboardDidShowNotification) 21 | .compactMap { _ in true } 22 | 23 | private let keyboardWillHide = 24 | NotificationCenter.default 25 | .publisher(for: UIResponder.keyboardDidHideNotification) 26 | .map { _ in false } 27 | 28 | init() { 29 | cancelable = Publishers.Merge(keyboardWillShow, keyboardWillHide) 30 | .subscribe(on: DispatchQueue.main) 31 | .assign(to: \.self.isVisible, on: self) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/discover/discover.nearby.imageset/person_circle_fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/myprofile.qrcode.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "IMG_3884.jpeg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | }, 16 | { 17 | "filename" : "IMG_3899.jpeg", 18 | "idiom" : "universal", 19 | "locale" : "zh-Hans", 20 | "scale" : "1x" 21 | }, 22 | { 23 | "idiom" : "universal", 24 | "locale" : "zh-Hans", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "idiom" : "universal", 29 | "locale" : "zh-Hans", 30 | "scale" : "3x" 31 | }, 32 | { 33 | "filename" : "IMG_3884-1.jpeg", 34 | "idiom" : "universal", 35 | "locale" : "en", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "universal", 40 | "locale" : "en", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "universal", 45 | "locale" : "en", 46 | "scale" : "3x" 47 | } 48 | ], 49 | "info" : { 50 | "author" : "xcode", 51 | "version" : 1 52 | }, 53 | "properties" : { 54 | "localizable" : true 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/discover/discover.search.imageset/ic_fireworks.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/discover/discover.shake.imageset/shake-it.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/UI/Me/QrCodeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QrCodeView.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/17. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct QrCodeView: View { 11 | @Environment(\.presentationMode) var presentationMode 12 | 13 | var body: some View { 14 | ZStack { 15 | Image(Asset.Image.myprofileQrcode.name) 16 | .resizable() 17 | .scaledToFit() 18 | .cornerRadius(8.0) 19 | } 20 | .padding(20) 21 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) 22 | .background(Asset.Color.backgroudGray.color) 23 | .navigationBarTitleDisplayMode(.inline) 24 | .navigationTitle(L10n.QrCode.title.key) 25 | .navigationBarBackButtonHidden(true) 26 | .toolbar { 27 | ToolbarItem(placement: .navigationBarLeading) { 28 | Button(action: { presentationMode.wrappedValue.dismiss() }) { 29 | Image(systemName: "chevron.backward") 30 | } 31 | } 32 | ToolbarItem(placement: .navigationBarTrailing){ 33 | Button(action: {}) { 34 | Image(systemName: "ellipsis") 35 | } 36 | } 37 | } 38 | } 39 | } 40 | 41 | struct QrCodeView_Previews: PreviewProvider { 42 | static var previews: some View { 43 | QrCodeView() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/UI/Chats/Detail/ChatInputView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatInputView.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/14. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ChatInputView: View { 11 | @State private var message: String = "" 12 | let onCommit: (String) -> Void 13 | 14 | var body: some View { 15 | HStack { 16 | Image(systemName: "speaker.wave.2.circle") 17 | .chatInputIconStyle() 18 | TextField("", text: $message, onCommit: { 19 | onCommit(message) 20 | message = "" 21 | }) 22 | .padding(8) 23 | .accentColor(Asset.Color.accentColor.color) 24 | .background(Color.white) 25 | .clipShape(RoundedRectangle(cornerRadius: 4)) 26 | .frame(maxWidth: .infinity) 27 | 28 | Image(systemName: "face.smiling") 29 | .chatInputIconStyle() 30 | Image(systemName: "plus.circle") 31 | .chatInputIconStyle() 32 | } 33 | .padding(8) 34 | .background(Asset.Color.tabbarGray.color) 35 | } 36 | } 37 | 38 | private extension Image { 39 | func chatInputIconStyle() -> some View { 40 | return self.font(.system(size: 28, weight: .light, design: .default)) 41 | } 42 | } 43 | 44 | struct ChatInputView_Previews: PreviewProvider { 45 | static var previews: some View { 46 | ChatInputView(onCommit: { _ in }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/UI/Contacts/ContactItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactItemView.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/17. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContactItemView: View { 11 | let contact: Contact 12 | 13 | var body: some View { 14 | HStack() { 15 | Image(contact.profileImage) 16 | .resizable() 17 | .scaledToFit() 18 | .frame(width: 40, height: 40, alignment: .center) 19 | .cornerRadius(4.0) 20 | Spacer(minLength: 15) 21 | VStack(alignment: .leading) { 22 | Spacer() 23 | Text(contact.name.key, 24 | tableName: contact.name.table) 25 | .font(.title3) 26 | .lineLimit(1) 27 | .frame(maxWidth: .infinity, alignment: .leading) 28 | Spacer() 29 | } 30 | } 31 | .padding(.trailing) 32 | .frame(height: 60, alignment: .leading) 33 | .frame(maxWidth: .infinity) 34 | } 35 | } 36 | 37 | struct ContactItemView_Previews: PreviewProvider { 38 | static let contacts = getDefaultContactList().shuffled() 39 | 40 | static var previews: some View { 41 | ScrollView { 42 | ContactItemView(contact: contacts[0]) 43 | ContactItemView(contact: contacts[1]) 44 | ContactItemView(contact: contacts[2]) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/UI/Chats/Detail/ChatDetailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatDetailView.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/14. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ChatDetailView: View { 11 | @State var chat: Chat 12 | @StateObject var viewModel: ChatDetailViewModel 13 | @Environment(\.presentationMode) var presentationMode 14 | 15 | var body: some View { 16 | VStack(spacing: 0) { 17 | ChatFlowView(messageFlow: $viewModel.messageFlow).onAppear() { 18 | viewModel.subscribe() 19 | viewModel.sendLastMessage(chat.lastMessage) 20 | } 21 | ChatInputView { message in 22 | viewModel.send(message) 23 | viewModel.echo(message) 24 | } 25 | } 26 | .navigationTitle(Text(chat.contact.name.key, 27 | tableName: chat.contact.name.table)) 28 | .navigationBarBackButtonHidden(true) 29 | .toolbar { 30 | ToolbarItem(placement: .navigationBarLeading) { 31 | Button(action: { presentationMode.wrappedValue.dismiss() }) { 32 | Image(systemName: "chevron.backward") 33 | } 34 | } 35 | ToolbarItem(placement: .navigationBarTrailing) { 36 | Button(action: {}) { 37 | Image(systemName: "ellipsis") 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | struct ChatDetailView_Previews: PreviewProvider { 45 | static let chat = getDefaultChatList()[0] 46 | static var previews: some View { 47 | ChatDetailView(chat: chat, 48 | viewModel: ChatDetailViewModel(opposite: chat.contact)) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/UI/Chats/ChatsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatsView.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/14. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ChatsView: View { 11 | let chats = getDefaultChatList() 12 | var body: some View { 13 | NavigationView { 14 | ScrollView(showsIndicators: false) { 15 | VStack(spacing:0) { 16 | SearchBarView() 17 | ForEach(chats, id: \.self) { chat in 18 | NavigationLink(destination: createChatDetailView(with: chat)) { 19 | ChatItemView(chat: chat) 20 | } 21 | } 22 | } 23 | } 24 | .navigationBarTitleDisplayMode(.inline) 25 | .navigationTitle(L10n.appName.key) 26 | .toolbar { 27 | ToolbarItem(placement: .navigationBarLeading){ 28 | Button(action: {}) { 29 | Image(systemName: "ellipsis") 30 | } 31 | } 32 | ToolbarItem(placement: .navigationBarTrailing){ 33 | Button(action: {}) { 34 | Image(systemName: "plus.circle") 35 | } 36 | } 37 | } 38 | } 39 | .accentColor(.black) 40 | } 41 | } 42 | 43 | private func createChatDetailView(with chat: Chat) -> some View { 44 | ChatDetailView(chat: chat, 45 | viewModel: ChatDetailViewModel( 46 | opposite: chat.contact 47 | ) 48 | ) 49 | } 50 | 51 | struct ChatsView_Previews: PreviewProvider { 52 | static var previews: some View { 53 | ChatsView() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/discover/discover.moments.imageset/shutter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/UI/Chats/Detail/ChatFlowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatFlowView.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/14. 6 | // 7 | 8 | import Combine 9 | import SwiftUI 10 | 11 | struct ChatFlowView: View { 12 | @Binding var messageFlow: [ChatMessage] 13 | @StateObject private var keyboard = KeyboardVisibilityReporter() 14 | 15 | var body: some View { 16 | ScrollViewReader { scrollView in 17 | ScrollView(showsIndicators: false) { 18 | VStack(spacing:0) { 19 | ForEach(messageFlow, id: \.self) { message in 20 | ChatMessageView(message: message).onAppear() { 21 | scrollView.scrollTo(message) 22 | } 23 | } 24 | }.onChange(of: keyboard.isVisible) { isVisible in 25 | if isVisible { 26 | scrollView.scrollTo(messageFlow.last!) 27 | } 28 | } 29 | }.background(Asset.Color.chatGray.color) 30 | } 31 | } 32 | } 33 | 34 | struct ChatFlowView_Previews: PreviewProvider { 35 | static let opposite = getDefaultContactList().shuffled()[0] 36 | static let messages = getDefaultChatList().map(\.lastMessage).shuffled() 37 | static let chatMessages = [ 38 | ChatMessage(side: .Left(profileImage: opposite.profileImage), text: .LocalizedString(text: messages[0])), 39 | ChatMessage(side: .Right, text: .LocalizedString(text: messages[1])), 40 | ChatMessage(side: .Left(profileImage: opposite.profileImage), text: .LocalizedString(text: messages[2])), 41 | ChatMessage(side: .Right, text: .LocalizedString(text: messages[3])) 42 | ] 43 | static var previews: some View { 44 | ChatFlowView(messageFlow: .constant(chatMessages)) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Data/ContactList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactList.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/14. 6 | // 7 | 8 | import Foundation 9 | 10 | func getDefaultContactList() -> [Contact] { 11 | Friends.allCases.map(\.contact) 12 | } 13 | 14 | struct Contact: Hashable { 15 | let profileImage: String, name: LocalizedString 16 | } 17 | 18 | private enum Friends: CaseIterable { 19 | case Antony, Apple, Bosco, Joe, Microsoft, Muraosa, Ron, Saha, Sony, UncleWang 20 | 21 | var contact: Contact { 22 | switch self { 23 | case .Antony: 24 | return Contact(profileImage: Asset.Image.avatar01.name, name: L10nExtra.Contact.Name.antony) 25 | case .Apple: 26 | return Contact(profileImage: Asset.Image.avatar02.name, name: L10nExtra.Contact.Name.apple) 27 | case .Bosco: 28 | return Contact(profileImage: Asset.Image.avatar03.name, name: L10nExtra.Contact.Name.bosco) 29 | case .Joe: 30 | return Contact(profileImage: Asset.Image.avatar04.name, name: L10nExtra.Contact.Name.joe) 31 | case .Microsoft: 32 | return Contact(profileImage: Asset.Image.avatar05.name, name: L10nExtra.Contact.Name.microsoft) 33 | case .Muraosa: 34 | return Contact(profileImage: Asset.Image.avatar06.name, name: L10nExtra.Contact.Name.muraosa) 35 | case .Ron: 36 | return Contact(profileImage: Asset.Image.avatar07.name, name: L10nExtra.Contact.Name.ron) 37 | case .Saha: 38 | return Contact(profileImage: Asset.Image.avatar08.name, name: L10nExtra.Contact.Name.saha) 39 | case .Sony: 40 | return Contact(profileImage: Asset.Image.avatar09.name, name: L10nExtra.Contact.Name.sony) 41 | case .UncleWang: 42 | return Contact(profileImage: Asset.Image.avatar10.name, name: L10nExtra.Contact.Name.uncleWang) 43 | } 44 | } 45 | } 46 | 47 | 48 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Data/ChatList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatList.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/14. 6 | // 7 | 8 | import Foundation 9 | 10 | func getDefaultChatList() -> [Chat] { 11 | let contacts = getDefaultContactList().shuffled() 12 | let lastMessages = [ 13 | L10nExtra.LastMessage.Message.hi, 14 | L10nExtra.LastMessage.Message.hey, 15 | L10nExtra.LastMessage.Message.hello, 16 | L10nExtra.LastMessage.Message.niceToMeetYou, 17 | L10nExtra.LastMessage.Message.howAreYou, 18 | L10nExtra.LastMessage.Message.aloha, 19 | L10nExtra.LastMessage.Message.areYouStillThere, 20 | L10nExtra.LastMessage.Message.iLoveYou, 21 | L10nExtra.LastMessage.Message.theMatrixIsLookingForYou, 22 | L10nExtra.LastMessage.Message.iAmRobot 23 | ].shuffled() 24 | let lastContactTimes: [LastContactTime] = [ 25 | .String(time: "13:53"), 26 | .String(time: "11:11"), 27 | .String(time: "03:23"), 28 | .LocalizedString(time: L10nExtra.LastMessage.DateTime.yesterday), 29 | .LocalizedString(time: L10nExtra.LastMessage.DateTime.monday), 30 | .LocalizedString(time: L10nExtra.LastMessage.DateTime.sunday), 31 | .String(time: "2021/04/01"), 32 | .String(time: "2021/01/01"), 33 | .String(time: "2020/11/11"), 34 | .String(time: "2020/02/14") 35 | ] 36 | var chatList: [Chat] = [] 37 | for index in 0..<10 { 38 | chatList.append(Chat(contact: contacts[index], 39 | lastMessage: lastMessages[index], 40 | lastContactTime: lastContactTimes[index])) 41 | } 42 | return chatList 43 | } 44 | 45 | struct Chat: Hashable { 46 | let contact: Contact 47 | var lastMessage: LocalizedString, lastContactTime: LastContactTime 48 | } 49 | 50 | enum LastContactTime: Hashable { 51 | case String(time: String), 52 | LocalizedString(time: LocalizedString) 53 | } 54 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/UI/Me/MeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MeView.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/14. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MeView: View { 11 | private let items = getMeItems() 12 | private let spaceHeight: CGFloat = 12 13 | 14 | var body: some View { 15 | NavigationView { 16 | ScrollView(showsIndicators: false) { 17 | VStack(spacing:0) { 18 | NavigationLink(destination: QrCodeView()) { 19 | MyProfileView() 20 | } 21 | Group { 22 | Spacer(minLength: spaceHeight) 23 | LanguageSwitcher() 24 | Spacer(minLength: spaceHeight) 25 | } 26 | Group { 27 | items[.Pay]?.toItemBarView() 28 | Spacer(minLength: spaceHeight) 29 | } 30 | Group { 31 | items[.Favorites]?.toItemBarView(withDivider: true) 32 | items[.Moments]?.toItemBarView(withDivider: true) 33 | items[.Channels]?.toItemBarView(withDivider: true) 34 | items[.Cards]?.toItemBarView(withDivider: true) 35 | items[.Sticker]?.toItemBarView() 36 | Spacer(minLength: spaceHeight) 37 | } 38 | Group { 39 | items[.Settings]?.toItemBarView() 40 | } 41 | Spacer() 42 | } 43 | } 44 | .background(Asset.Color.backgroudGray.color) 45 | .navigationBarTitleDisplayMode(.inline) 46 | .navigationBarHidden(true) 47 | } 48 | .accentColor(.black) 49 | } 50 | } 51 | 52 | struct MeView_Previews: PreviewProvider { 53 | static var previews: some View { 54 | MeView() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | CFBundleDisplayName 22 | $(PRODUCT_NAME) 23 | LSRequiresIPhoneOS 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | 30 | UIApplicationSupportsIndirectInputEvents 31 | 32 | UILaunchScreen 33 | 34 | UIImageName 35 | Splash 36 | 37 | UIRequiredDeviceCapabilities 38 | 39 | armv7 40 | 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UISupportedInterfaceOrientations~ipad 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationPortraitUpsideDown 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | LSApplicationCategoryType 55 | public.app-category.social-networking 56 | 57 | 58 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/UI/Discover/DiscoverView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiscoverView.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/14. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DiscoverView: View { 11 | private let items = getDiscoverItems() 12 | private let spaceHeight: CGFloat = 12 13 | 14 | var body: some View { 15 | NavigationView { 16 | ScrollView(showsIndicators: false) { 17 | VStack(spacing:0) { 18 | Group { 19 | items[.Moments]?.toItemBarView() 20 | Spacer(minLength: spaceHeight) 21 | } 22 | Group { 23 | items[.Channels]?.toItemBarView(withDivider: true) 24 | items[.Live]?.toItemBarView() 25 | Spacer(minLength: spaceHeight) 26 | } 27 | Group { 28 | items[.Scan]?.toItemBarView(withDivider: true) 29 | items[.Shake]?.toItemBarView() 30 | Spacer(minLength: spaceHeight) 31 | } 32 | Group { 33 | items[.TopStories]?.toItemBarView(withDivider: true) 34 | items[.Search]?.toItemBarView() 35 | Spacer(minLength: spaceHeight) 36 | } 37 | Group { 38 | items[.Nearby]?.toItemBarView() 39 | Spacer(minLength: spaceHeight) 40 | } 41 | Group { 42 | items[.MiniPrograms]?.toItemBarView() 43 | } 44 | Spacer() 45 | } 46 | } 47 | .background(Asset.Color.backgroudGray.color) 48 | .navigationBarTitleDisplayMode(.inline) 49 | .navigationTitle(L10n.Tabs.discover.key) 50 | } 51 | } 52 | } 53 | 54 | struct DiscoverView_Previews: PreviewProvider { 55 | static var previews: some View { 56 | DiscoverView() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/UI/Chats/Detail/ViewModel/ChatDetailViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatDetailViewModel.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/15. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | class ChatDetailViewModel : ObservableObject { 12 | private let echo = PassthroughSubject() 13 | private var subscription: AnyCancellable? = nil 14 | 15 | @Published var messageFlow: [ChatMessage] 16 | let opposite: Contact 17 | 18 | init(messageFlow: [ChatMessage] = [], opposite: Contact) { 19 | self.messageFlow = messageFlow 20 | self.opposite = opposite 21 | } 22 | 23 | func subscribe() { 24 | subscription = echo.debounce(for: .seconds(1), scheduler: DispatchQueue.main) 25 | .sink(receiveValue: { [weak self] message in 26 | self?.messageFlow.append(message) 27 | }) 28 | } 29 | 30 | func sendLastMessage(_ message: LocalizedString) { 31 | messageFlow.append( 32 | ChatMessage(side: .Left(profileImage: opposite.profileImage), 33 | text: .LocalizedString(text: message))) 34 | } 35 | 36 | func send(_ message: String) { 37 | messageFlow.append(ChatMessage(side: .Right, text: .String(text: message))) 38 | } 39 | 40 | func echo(_ message: String) { 41 | echo.send( 42 | ChatMessage(side: .Left(profileImage: opposite.profileImage), 43 | text: .String(text: message.stupidSwap()))) 44 | } 45 | 46 | deinit { 47 | subscription?.cancel() 48 | } 49 | } 50 | 51 | private extension String { 52 | func swap(_ a: String, _ b: String) -> String { 53 | let swapMark = "__Swapping__" 54 | let replaceA = self.replacingOccurrences(of: a, with: swapMark) 55 | let replaceB = replaceA.replacingOccurrences(of: b, with: a) 56 | return replaceB.replacingOccurrences(of: swapMark, with: b) 57 | } 58 | 59 | func stupidSwap() -> String { 60 | return self.swap("我", "你") 61 | .swap("I ", "You ") 62 | .swap(" you", " me") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/UI/TabContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabContainer.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/14. 6 | // 7 | 8 | import Foundation 9 | 10 | import SwiftUI 11 | 12 | struct TabContainer: View { 13 | @State var selectingTab: Tabs = Tabs.Chats 14 | 15 | var body: some View { 16 | TabView(selection: $selectingTab){ 17 | ChatsView().tabItem { 18 | Image(systemName: Tabs.Chats.currentStateIconName(selecting: selectingTab)) 19 | Text(L10n.Tabs.chats.key) 20 | }.tag(Tabs.Chats) 21 | ContactsView().tabItem { 22 | Image(systemName: Tabs.Contacts.currentStateIconName(selecting: selectingTab)) 23 | Text(L10n.Tabs.contacts.key) 24 | }.tag(Tabs.Contacts) 25 | DiscoverView().tabItem { 26 | Image(systemName: Tabs.Discover.currentStateIconName(selecting: selectingTab)) 27 | Text(L10n.Tabs.discover.key) 28 | }.tag(Tabs.Discover) 29 | MeView().tabItem { 30 | Image(systemName: Tabs.Me.currentStateIconName(selecting: selectingTab)) 31 | Text(L10n.Tabs.me.key) 32 | }.tag(Tabs.Me) 33 | }.edgesIgnoringSafeArea(.all) 34 | } 35 | } 36 | 37 | enum Tabs { 38 | case Chats, Contacts, Discover, Me 39 | 40 | struct IconName: Hashable { 41 | let on: String, off: String 42 | } 43 | 44 | var iconName: IconName { 45 | switch self { 46 | case .Chats: 47 | return IconName(on: "message.fill", off: "message") 48 | case .Contacts: 49 | return IconName(on: "person.2.fill", off: "person.2") 50 | case .Discover: 51 | return IconName(on: "safari.fill", off: "safari") 52 | case .Me: 53 | return IconName(on: "person.fill", off: "person") 54 | } 55 | } 56 | 57 | func currentStateIconName(selecting: Tabs) -> String { 58 | selecting == self ? iconName.on : iconName.off 59 | } 60 | } 61 | 62 | struct TabContainer_Previews: PreviewProvider { 63 | static var previews: some View { 64 | TabContainer() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/discover/discover.channels.imageset/butterfly_f.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/UI/Me/MyProfileView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyProfileView.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/17. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MyProfileView: View { 11 | private let profile = MyProfile() 12 | 13 | var body: some View { 14 | HStack{ 15 | Image(profile.profileImage) 16 | .resizable() 17 | .scaledToFit() 18 | .frame(width: 70, height: 70, alignment: .center) 19 | .cornerRadius(8.0) 20 | .padding(.horizontal, 10) 21 | VStack(alignment: .leading, spacing: 10) { 22 | Text(L10n.MyProfile.name.key) 23 | .font(.title2) 24 | (Text(L10n.MyProfile.weChatID.key)+Text(profile.weChatID)) 25 | .font(.body) 26 | .foregroundColor(Color.gray) 27 | .lineLimit(1) 28 | (Text(Image(systemName: "plus"))+Text(L10n.MyProfile.status.key)) 29 | .font(.callout) 30 | .fontWeight(.light) 31 | .foregroundColor(Color.gray) 32 | .padding(2) 33 | .padding(.trailing, 8) 34 | .overlay(Capsule() 35 | .stroke(lineWidth: 0.5) 36 | .foregroundColor(Asset.Color.lightGray.color)) 37 | }.offset(x: 0, y: 16.0) 38 | Spacer() 39 | Group { 40 | Color.gray.mask( 41 | Image(Asset.Image.myprofileQrcodeIcon.name) 42 | .resizable() 43 | .scaledToFit() 44 | ).frame(width: 20, height: 20) 45 | Image(systemName: "chevron.forward") 46 | .resizable() 47 | .scaledToFit() 48 | .frame(width: 18, height: 18, alignment: .center) 49 | .foregroundColor(Asset.Color.lightGray.color) 50 | }.offset(x: 0, y: 18.0) 51 | } 52 | .padding() 53 | .padding(.top, 20) 54 | .padding(.bottom, 36) 55 | .frame(maxWidth: .infinity) 56 | .background(Color.white) 57 | } 58 | } 59 | 60 | struct MyProfileView_Previews: PreviewProvider { 61 | static var previews: some View { 62 | ScrollView { 63 | MyProfileView() 64 | .overlay(Rectangle().stroke()) 65 | }.background(Color.gray) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/swift 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift 3 | 4 | ### Swift ### 5 | # Xcode 6 | # 7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 8 | 9 | ## User settings 10 | xcuserdata/ 11 | 12 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 13 | *.xcscmblueprint 14 | *.xccheckout 15 | 16 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 17 | build/ 18 | DerivedData/ 19 | *.moved-aside 20 | *.pbxuser 21 | !default.pbxuser 22 | *.mode1v3 23 | !default.mode1v3 24 | *.mode2v3 25 | !default.mode2v3 26 | *.perspectivev3 27 | !default.perspectivev3 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | 32 | ## App packaging 33 | *.ipa 34 | *.dSYM.zip 35 | *.dSYM 36 | 37 | ## Playgrounds 38 | timeline.xctimeline 39 | playground.xcworkspace 40 | 41 | # Swift Package Manager 42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 43 | # Packages/ 44 | # Package.pins 45 | # Package.resolved 46 | # *.xcodeproj 47 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 48 | # hence it is not needed unless you have added a package configuration file to your project 49 | # .swiftpm 50 | 51 | .build/ 52 | 53 | # CocoaPods 54 | # We recommend against adding the Pods directory to your .gitignore. However 55 | # you should judge for yourself, the pros and cons are mentioned at: 56 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 57 | # Pods/ 58 | # Add this line if you want to avoid checking in source code from the Xcode workspace 59 | # *.xcworkspace 60 | 61 | # Carthage 62 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 63 | # Carthage/Checkouts 64 | 65 | Carthage/Build/ 66 | 67 | # Accio dependency management 68 | Dependencies/ 69 | .accio/ 70 | 71 | # fastlane 72 | # It is recommended to not store the screenshots in the git repo. 73 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 74 | # For more information about the recommended setup visit: 75 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 76 | 77 | fastlane/report.xml 78 | fastlane/Preview.html 79 | fastlane/screenshots/**/*.png 80 | fastlane/test_output 81 | 82 | # Code Injection 83 | # After new code Injection tools there's a generated folder /iOSInjectionProject 84 | # https://github.com/johnno1962/injectionforxcode 85 | 86 | iOSInjectionProject/ 87 | 88 | # End of https://www.toptal.com/developers/gitignore/api/swift 89 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/discover/discover.miniPrograms.imageset/science-dna.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/UI/Chats/Detail/ChatMessageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatMessage.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/14. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ChatMessageView: View { 11 | let message: ChatMessage 12 | 13 | var body: some View { 14 | HStack(alignment: .top) { 15 | Image(message.side.profileImage) 16 | .resizable() 17 | .scaledToFit() 18 | .frame(width: 40, height: 40, alignment: .center) 19 | .cornerRadius(4.0) 20 | MessageText(message: message) 21 | Spacer() 22 | } 23 | .padding(10) 24 | .environment(\.layoutDirection, message.side.direction) 25 | } 26 | } 27 | 28 | fileprivate extension Text { 29 | func messageStyle(side: Side) -> some View { 30 | self.padding(10) 31 | .background(side.color) 32 | .clipShape(RoundedRectangle(cornerRadius: 4)) 33 | .environment(\.layoutDirection, .leftToRight) 34 | } 35 | } 36 | 37 | fileprivate struct MessageText: View { 38 | let message: ChatMessage 39 | var body: some View { 40 | switch message.text { 41 | case .String(let text): 42 | Text(text).messageStyle(side: message.side) 43 | case .LocalizedString(let text): 44 | Text(text.key, tableName: text.table) 45 | .messageStyle(side: message.side) 46 | } 47 | } 48 | } 49 | 50 | extension Side { 51 | var color: Color { 52 | switch self { 53 | case .Left: 54 | return Color.white 55 | case .Right: 56 | return Asset.Color.chatGreen.color 57 | } 58 | } 59 | 60 | var direction: LayoutDirection { 61 | switch self { 62 | case .Left: 63 | return .leftToRight 64 | case .Right: 65 | return .rightToLeft 66 | } 67 | } 68 | } 69 | 70 | struct ChatMessageView_Previews: PreviewProvider { 71 | static let contact: Contact = getDefaultContactList().shuffled()[0] 72 | 73 | static var previews: some View { 74 | ScrollView() { 75 | VStack(spacing: 0) { 76 | ChatMessageView(message: ChatMessage(side: .Left(profileImage: contact.profileImage), text: .String(text: "Hello?"))) 77 | ChatMessageView(message: ChatMessage(side: .Right, text: .String(text: "Hello?"))) 78 | ChatMessageView(message: ChatMessage(side: .Left(profileImage: contact.profileImage), text: .String(text: "Hello?Hello?Hello?Hello?Hello?Hello?Hello?Hello?Hello?Hello?Hello?Hello?Hello?Hello?"))) 79 | ChatMessageView(message: ChatMessage(side: .Right, text: .String(text: "Hello?Hello?Hello?Hello?Hello?Hello?Hello?Hello?Hello?Hello?Hello?Hello?"))) 80 | } 81 | } 82 | .background(Asset.Color.lightGray.color) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/UI/Me/Data/MeItems.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MeItems.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/17. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | func getMeItems() -> [MeItemName : ItemBarInfo] { 12 | return [ 13 | .Pay: ItemBarInfo( 14 | icon: Asset.Image.mePay.name, 15 | iconPattern: IconPattern.Color(Asset.Color.payGreen.color), 16 | title: L10n.Me.pay.key, 17 | name: nil, 18 | profileImage: nil), 19 | .Favorites: ItemBarInfo( 20 | icon: Asset.Image.meFavorites.name, 21 | iconPattern: IconPattern.AngularGradient( 22 | AngularGradient(gradient: Gradient( 23 | colors: [ 24 | Asset.Color.favoritesRed.color, 25 | Asset.Color.favoritesYellow.color, 26 | Asset.Color.favoritesYellow.color, 27 | Asset.Color.favoritesBlue.color, 28 | Asset.Color.favoritesRed.color 29 | ]), 30 | center: .center, 31 | angle: Angle(degrees: 10))), 32 | title: L10n.Me.favorites.key, 33 | name: nil, 34 | profileImage: nil), 35 | .Moments: ItemBarInfo( 36 | icon: Asset.Image.meMoments.name, 37 | iconPattern: IconPattern.Color(Asset.Color.meMomentsBlue.color), 38 | title: L10n.Me.moments.key, 39 | name: nil, 40 | profileImage: nil), 41 | .Channels: ItemBarInfo( 42 | icon: Asset.Image.meChannels.name, 43 | iconPattern: IconPattern.Color(Asset.Color.channelsOrange.color), 44 | title: L10n.Me.channels.key, 45 | name: nil, 46 | profileImage: nil), 47 | .Cards: ItemBarInfo( 48 | icon: Asset.Image.meCards.name, 49 | iconPattern: IconPattern.Color(Asset.Color.cardsBlue.color), 50 | title: L10n.Me.cards.key, 51 | name: nil, 52 | profileImage: nil), 53 | .Sticker: ItemBarInfo( 54 | icon: Asset.Image.meSticker.name, 55 | iconPattern: IconPattern.Color(Asset.Color.stickerYellow.color), 56 | title: L10n.Me.sticker.key, 57 | name: nil, 58 | profileImage: nil), 59 | .Settings: ItemBarInfo( 60 | icon: Asset.Image.meSettings.name, 61 | iconPattern: IconPattern.Color(Asset.Color.settingsBlue.color), 62 | title: L10n.Me.settings.key, 63 | name: nil, 64 | profileImage: nil) 65 | ] 66 | } 67 | 68 | enum MeItemName { 69 | case Pay, Favorites, Moments, Channels, 70 | Cards, Sticker, Settings 71 | } 72 | 73 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/me.settings.imageset/Gear-2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/UI/Chats/ChatItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatItemView.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/14. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ChatItemView: View { 11 | @State var chat: Chat 12 | private let isShowBadge: Bool = Float.random(in: 0...1) > 0.45 13 | 14 | var body: some View { 15 | HStack() { 16 | Image(chat.contact.profileImage) 17 | .resizable() 18 | .scaledToFit() 19 | .frame(width: 50, height: 50, alignment: .center) 20 | .cornerRadius(4.0) 21 | .withBadge(isShowBadge) 22 | Spacer(minLength: 15) 23 | VStack(alignment: .leading) { 24 | Spacer() 25 | HStack(alignment: .top) { 26 | VStack(alignment: .leading, spacing: 8) { 27 | Text(chat.contact.name.key, 28 | tableName: chat.contact.name.table) 29 | .font(.title3) 30 | .lineLimit(1) 31 | .frame(alignment: .leading) 32 | Text(chat.lastMessage.key, 33 | tableName: chat.lastMessage.table) 34 | .font(.callout) 35 | .fontWeight(.light) 36 | .lineLimit(1) 37 | .foregroundColor(.gray) 38 | .frame(alignment: .leading) 39 | } 40 | Spacer() 41 | LastContactTimeText(time: chat.lastContactTime) 42 | } 43 | Spacer() 44 | Divider() 45 | } 46 | } 47 | .padding(.horizontal) 48 | .frame(maxWidth: .infinity, minHeight: 80, maxHeight: 80) 49 | } 50 | } 51 | 52 | fileprivate struct LastContactTimeText: View { 53 | let time: LastContactTime 54 | var body: some View { 55 | switch time { 56 | case .String(let time): 57 | return AnyView(Text(time) 58 | .lastContactTimeStyle()) 59 | case .LocalizedString(let time): 60 | return AnyView(Text(time.key, tableName: time.table) 61 | .lastContactTimeStyle()) 62 | } 63 | } 64 | } 65 | 66 | fileprivate extension Text { 67 | func lastContactTimeStyle() -> some View { 68 | self.font(.caption) 69 | .lineLimit(1) 70 | .foregroundColor(Asset.Color.lightGray.color) 71 | } 72 | } 73 | 74 | struct ChatItemView_Previews: PreviewProvider { 75 | static let chats = getDefaultChatList() 76 | static var previews: some View { 77 | ScrollView { 78 | ChatItemView(chat: chats[0]) 79 | ChatItemView(chat: chats[1]) 80 | ChatItemView(chat: chats[4]) 81 | ChatItemView(chat: chats[6]) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-20@2x.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "icon-20@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "icon-29.png", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "icon-29@2x.png", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "icon-29@3x.png", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "icon-40@2x.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "icon-40@3x.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "icon-60@2x.png", 47 | "idiom" : "iphone", 48 | "scale" : "2x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "icon-60@3x.png", 53 | "idiom" : "iphone", 54 | "scale" : "3x", 55 | "size" : "60x60" 56 | }, 57 | { 58 | "filename" : "icon-20-ipad.png", 59 | "idiom" : "ipad", 60 | "scale" : "1x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "icon-20@2x-ipad.png", 65 | "idiom" : "ipad", 66 | "scale" : "2x", 67 | "size" : "20x20" 68 | }, 69 | { 70 | "filename" : "icon-29-ipad.png", 71 | "idiom" : "ipad", 72 | "scale" : "1x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "icon-29@2x-ipad.png", 77 | "idiom" : "ipad", 78 | "scale" : "2x", 79 | "size" : "29x29" 80 | }, 81 | { 82 | "filename" : "icon-40.png", 83 | "idiom" : "ipad", 84 | "scale" : "1x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "icon-40@2x.png", 89 | "idiom" : "ipad", 90 | "scale" : "2x", 91 | "size" : "40x40" 92 | }, 93 | { 94 | "filename" : "icon-76.png", 95 | "idiom" : "ipad", 96 | "scale" : "1x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "icon-76@2x.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "76x76" 104 | }, 105 | { 106 | "filename" : "icon-83.5@2x.png", 107 | "idiom" : "ipad", 108 | "scale" : "2x", 109 | "size" : "83.5x83.5" 110 | }, 111 | { 112 | "filename" : "icon-1024.png", 113 | "idiom" : "ios-marketing", 114 | "scale" : "1x", 115 | "size" : "1024x1024" 116 | } 117 | ], 118 | "info" : { 119 | "author" : "xcode", 120 | "version" : 1 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Resources/Image.xcassets/Me/myprofile.qrcode.icon.imageset/qr-code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo.xcodeproj/xcshareddata/xcschemes/SwiftUI-WeChatDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/UI/Discover/Data/DiscoverItems.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiscoverItems.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/17. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | func getDiscoverItems() -> [DiscoverItemName : ItemBarInfo] { 12 | let contacts = getDefaultContactList().shuffled() 13 | return [ 14 | .Moments: ItemBarInfo( 15 | icon: Asset.Image.discoverMoments.name, 16 | iconPattern: IconPattern.AngularGradient( 17 | AngularGradient(gradient: Gradient( 18 | colors: [ 19 | Asset.Color.momentsBlue.color, 20 | Asset.Color.momentsGreen.color, 21 | Asset.Color.momentsYellow.color, 22 | Asset.Color.momentsRed.color, 23 | Asset.Color.momentsBlue.color 24 | ]), 25 | center: .center)), 26 | title: L10n.Discover.moments.key, 27 | name: nil, 28 | profileImage: contacts[0].profileImage), 29 | .Channels: ItemBarInfo( 30 | icon: Asset.Image.discoverChannels.name, 31 | iconPattern: IconPattern.Color(Asset.Color.channelsOrange.color), 32 | title: L10n.Discover.channels.key, 33 | name: contacts[1].name, 34 | profileImage: contacts[1].profileImage), 35 | .Live: ItemBarInfo( 36 | icon: Asset.Image.discoverLive.name, 37 | iconPattern: IconPattern.Color(Asset.Color.liveRed.color), 38 | title: L10n.Discover.live.key, 39 | name: L10n.Discover.Live.onAir, 40 | profileImage: contacts[2].profileImage), 41 | .Scan: ItemBarInfo( 42 | icon: Asset.Image.discoverScan.name, 43 | iconPattern: IconPattern.Color(Asset.Color.scanBlue.color), 44 | title: L10n.Discover.scan.key, 45 | name: nil, 46 | profileImage: nil), 47 | .Shake: ItemBarInfo( 48 | icon: Asset.Image.discoverShake.name, 49 | iconPattern: IconPattern.Color(Asset.Color.shakeBlue.color), 50 | title: L10n.Discover.shake.key, 51 | name: nil, 52 | profileImage: nil), 53 | .TopStories: ItemBarInfo( 54 | icon: Asset.Image.discoverTopStories.name, 55 | iconPattern: IconPattern.Color(Asset.Color.topStoriesYellow.color), 56 | title: L10n.Discover.topStories.key, 57 | name: nil, 58 | profileImage: nil), 59 | .Search: ItemBarInfo( 60 | icon: Asset.Image.discoverSearch.name, 61 | iconPattern: IconPattern.Color(Asset.Color.searchRed.color), 62 | title: L10n.Discover.search.key, 63 | name: nil, 64 | profileImage: nil), 65 | .Nearby: ItemBarInfo( 66 | icon: Asset.Image.discoverNearby.name, 67 | iconPattern: IconPattern.Color(Asset.Color.nearbyBlue.color), 68 | title: L10n.Discover.nearby.key, 69 | name: nil, 70 | profileImage: nil), 71 | .MiniPrograms: ItemBarInfo( 72 | icon: Asset.Image.discoverMiniPrograms.name, 73 | iconPattern: IconPattern.Color(Asset.Color.miniProgramsPurple.color), 74 | title: L10n.Discover.miniPrograms.key, 75 | name: nil, 76 | profileImage: nil) 77 | ] 78 | } 79 | 80 | enum DiscoverItemName { 81 | case Moments, Channels, Live, Scan, 82 | Shake, TopStories, Search, Nearby, 83 | MiniPrograms 84 | } 85 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Generated/stringsExtra.swift: -------------------------------------------------------------------------------- 1 | // Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen 2 | // Derived from https://github.com/SwiftGen/SwiftGen/issues/685#issuecomment-782893242 3 | // Origin see https://github.com/SwiftGen/SwiftGen/blob/stable/templates/strings/structured-swift5.stencil 4 | // 5 | // Example: 6 | // 7 | // var body: some View { 8 | // VStack { 9 | // Text(L10n.Login.PageTitle.header.key) // return LocalizedStringKey 10 | // CustomView(string: L10n.Login.Step1.title.text) // return String 11 | // } 12 | // } 13 | 14 | import SwiftUI 15 | 16 | // MARK: - Strings 17 | 18 | public enum L10nExtra { 19 | public enum Contact { 20 | public enum Name { 21 | /// Antony 22 | public static let antony = LocalizedString(table: "Contact", lookupKey: "Name.Antony") 23 | /// Apple 24 | public static let apple = LocalizedString(table: "Contact", lookupKey: "Name.Apple") 25 | /// Bosco 26 | public static let bosco = LocalizedString(table: "Contact", lookupKey: "Name.Bosco") 27 | /// Joe 28 | public static let joe = LocalizedString(table: "Contact", lookupKey: "Name.Joe") 29 | /// Microsoft 30 | public static let microsoft = LocalizedString(table: "Contact", lookupKey: "Name.Microsoft") 31 | /// Muraosa 32 | public static let muraosa = LocalizedString(table: "Contact", lookupKey: "Name.Muraosa") 33 | /// Ron 34 | public static let ron = LocalizedString(table: "Contact", lookupKey: "Name.Ron") 35 | /// Saha 36 | public static let saha = LocalizedString(table: "Contact", lookupKey: "Name.Saha") 37 | /// Sony 38 | public static let sony = LocalizedString(table: "Contact", lookupKey: "Name.Sony") 39 | /// UncleWang 40 | public static let uncleWang = LocalizedString(table: "Contact", lookupKey: "Name.UncleWang") 41 | } 42 | } 43 | public enum LastMessage { 44 | public enum DateTime { 45 | /// Monday 46 | public static let monday = LocalizedString(table: "LastMessage", lookupKey: "DateTime.Monday") 47 | /// Sunday 48 | public static let sunday = LocalizedString(table: "LastMessage", lookupKey: "DateTime.Sunday") 49 | /// Yesterday 50 | public static let yesterday = LocalizedString(table: "LastMessage", lookupKey: "DateTime.Yesterday") 51 | } 52 | public enum Message { 53 | /// Aloha 54 | public static let aloha = LocalizedString(table: "LastMessage", lookupKey: "Message.Aloha") 55 | /// Are you still there? 56 | public static let areYouStillThere = LocalizedString(table: "LastMessage", lookupKey: "Message.AreYouStillThere") 57 | /// Hello 58 | public static let hello = LocalizedString(table: "LastMessage", lookupKey: "Message.Hello") 59 | /// Hey 60 | public static let hey = LocalizedString(table: "LastMessage", lookupKey: "Message.Hey") 61 | /// Hi 62 | public static let hi = LocalizedString(table: "LastMessage", lookupKey: "Message.Hi") 63 | /// How are you? 64 | public static let howAreYou = LocalizedString(table: "LastMessage", lookupKey: "Message.HowAreYou") 65 | /// I am robot. 66 | public static let iAmRobot = LocalizedString(table: "LastMessage", lookupKey: "Message.IAmRobot") 67 | /// I love you! 68 | public static let iLoveYou = LocalizedString(table: "LastMessage", lookupKey: "Message.ILoveYou") 69 | /// Nice to meet you~ 70 | public static let niceToMeetYou = LocalizedString(table: "LastMessage", lookupKey: "Message.NiceToMeetYou") 71 | /// The Matrix is looking for you. 72 | public static let theMatrixIsLookingForYou = LocalizedString(table: "LastMessage", lookupKey: "Message.TheMatrixIsLookingForYou") 73 | } 74 | } 75 | } 76 | 77 | // MARK: - Implementation Details 78 | 79 | private final class BundleToken { 80 | static let bundle: Bundle = { 81 | #if SWIFT_PACKAGE 82 | return Bundle.module 83 | #else 84 | return Bundle(for: BundleToken.self) 85 | #endif 86 | }() 87 | } 88 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/UI/Contacts/ContactsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactsView.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/14. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContactsView: View { 11 | @Environment(\.locale) var locale 12 | 13 | private var contactGroups: [ContactGroup] { 14 | getDefaultContactList() 15 | .group(withLocale: locale) 16 | .sortedList() 17 | } 18 | 19 | var body: some View { 20 | NavigationView { 21 | VStack(spacing: 0) { 22 | SearchBarView() 23 | List { 24 | ForEach(contactGroups, id: \.self) { group in 25 | // Completions will crash if ForEach nest together 26 | ContactSection(contactGroup: group) 27 | } 28 | } 29 | .listStyle(PlainListStyle()) 30 | } 31 | .background(Asset.Color.backgroudGray.color) 32 | .navigationBarTitleDisplayMode(.inline) 33 | .navigationTitle(L10n.Tabs.contacts.key) 34 | .toolbar { 35 | ToolbarItem(placement: .navigationBarTrailing){ 36 | Button(action: {}) { 37 | Image(systemName: "person.badge.plus") 38 | } 39 | } 40 | } 41 | }.accentColor(.black) 42 | } 43 | } 44 | 45 | private struct ContactSection: View { 46 | let contactGroup: ContactGroup 47 | 48 | var body: some View { 49 | Section(header: Text(String(contactGroup.groupKey)).fontWeight(.regular)) { 50 | ForEach(contactGroup.groupValue, id: \.self) { contact in 51 | ContactItemView(contact: contact) 52 | } 53 | } 54 | } 55 | } 56 | 57 | private extension Array where Element == Contact { 58 | /** 59 | Random contact array to random dictionary. 60 | 61 | `[Contact] -> [Character : [Contact]]` 62 | 63 | Example: 64 | ``` 65 | [Contact(China), Contact(Japan), Contact(America), Contact(Chile)] 66 | ↓ 67 | ["C": [Contact(Chile), Contact(China)], 68 | "J": [Contact(Japan)], 69 | "A": [Contact(America)]] 70 | ``` 71 | */ 72 | func group(withLocale locale: Locale) -> [Character : [Contact]] { 73 | let sorted = sorted { 74 | $0.name.text(withLocale: locale) < $1.name.text(withLocale: locale) 75 | } 76 | var sortedDict: [Character : [Contact]] = [:] 77 | for contact in sorted { 78 | guard let key = contact.name.text(withLocale: locale).first else { 79 | continue 80 | } 81 | var group = sortedDict[key] ?? [] 82 | group.append(contact) 83 | sortedDict[key] = group 84 | } 85 | return sortedDict 86 | } 87 | } 88 | 89 | private extension Dictionary where Key == Character, Value == [Contact] { 90 | /** 91 | Random dictionary to sorted array of contact's group. 92 | 93 | `[Character : [Contact]] -> [ContactGroup]` 94 | 95 | Example: 96 | ``` 97 | ["C": [Contact(Chile), Contact(China)], 98 | "J": [Contact(Japan)], 99 | "A": [Contact(America)]] 100 | ↓ 101 | [ContactGroup("A", [Contact(America)]), 102 | ContactGroup("C", [Contact(Chile), Contact(China)]), 103 | ContactGroup("J", [Contact(Japan)])] 104 | ``` 105 | */ 106 | func sortedList() -> [ContactGroup] { 107 | var sortedList: [ContactGroup] = [] 108 | let sortedKeys = keys.sorted() 109 | for key in sortedKeys { 110 | if let contacts = self[key] { 111 | sortedList.append( 112 | ContactGroup(groupKey: key, 113 | groupValue: contacts) 114 | ) 115 | } 116 | } 117 | return sortedList 118 | } 119 | } 120 | 121 | private struct ContactGroup: Hashable { 122 | let groupKey: Character 123 | let groupValue: [Contact] 124 | } 125 | 126 | struct ContactsView_Previews: PreviewProvider { 127 | static var previews: some View { 128 | ContactsView() 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /swiftui-strings-template.stencil: -------------------------------------------------------------------------------- 1 | // Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen 2 | // Derived from https://github.com/SwiftGen/SwiftGen/issues/685#issuecomment-782893242 3 | // Origin see https://github.com/SwiftGen/SwiftGen/blob/stable/templates/strings/structured-swift5.stencil 4 | // 5 | // Example: 6 | // 7 | // var body: some View { 8 | // VStack { 9 | // Text(L10n.Login.PageTitle.header.key) // return LocalizedStringKey 10 | // CustomView(string: L10n.Login.Step1.title.text) // return String 11 | // } 12 | // } 13 | 14 | {% if tables.count > 0 %} 15 | import SwiftUI 16 | 17 | // MARK: - Strings 18 | 19 | {% macro parametersBlock types %}{% filter removeNewlines:"leading" %} 20 | {% for type in types %} 21 | {% if type == "String" %} 22 | _ p{{forloop.counter}}: Any 23 | {% else %} 24 | _ p{{forloop.counter}}: {{type}} 25 | {% endif %} 26 | {{ ", " if not forloop.last }} 27 | {% endfor %} 28 | {% endfilter %}{% endmacro %} 29 | {% macro argumentsBlock types %}{% filter removeNewlines:"leading" %} 30 | {% for type in types %} 31 | {% if type == "String" %} 32 | String(describing: p{{forloop.counter}}) 33 | {% elif type == "UnsafeRawPointer" %} 34 | Int(bitPattern: p{{forloop.counter}}) 35 | {% else %} 36 | p{{forloop.counter}} 37 | {% endif %} 38 | {{ ", " if not forloop.last }} 39 | {% endfor %} 40 | {% endfilter %}{% endmacro %} 41 | {% macro recursiveBlock table item %} 42 | {% for string in item.strings %} 43 | {% if not param.noComments %} 44 | /// {{string.translation}} 45 | {% endif %} 46 | {% if string.types %} 47 | public static func {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}({% call parametersBlock string.types %}) -> String { 48 | return tr("{{table}}", "{{string.key}}", {% call argumentsBlock string.types %}) 49 | } 50 | {% else %} 51 | public static let {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = LocalizedString(table: "{{table}}", lookupKey: "{{string.key}}") 52 | {% endif %} 53 | {% endfor %} 54 | {% for child in item.children %} 55 | public enum {{child.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { 56 | {% filter indent:2 %}{% call recursiveBlock table child %}{% endfilter %} 57 | } 58 | {% endfor %} 59 | {% endmacro %} 60 | {% set enumName %}{{param.enumName|default:"L10n"}}{% endset %} 61 | public enum {{enumName}} { 62 | {% if tables.count > 1 or param.forceFileNameEnum %} 63 | {% for table in tables %} 64 | public enum {{table.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { 65 | {% filter indent:2 %}{% call recursiveBlock table.name table.levels %}{% endfilter %} 66 | } 67 | {% endfor %} 68 | {% else %} 69 | {% call recursiveBlock tables.first.name tables.first.levels %} 70 | {% endif %} 71 | } 72 | 73 | // MARK: - Implementation Details 74 | {% if not param.withoutSupporter %} 75 | fileprivate func tr(_ table: String, _ key: String, _ locale: Locale = Locale.current, _ args: CVarArg...) -> String { 76 | let path = Bundle.main.path(forResource: locale.identifier, ofType: "lproj") ?? "" 77 | let format: String 78 | if let bundle = Bundle(path: path) { 79 | format = NSLocalizedString(key, tableName: table, bundle: bundle, comment: "") 80 | } else { 81 | format = BundleToken.bundle.localizedString(forKey: key, value: nil, table: table) 82 | } 83 | return String(format: format, locale: locale, arguments: args) 84 | } 85 | 86 | public struct LocalizedString: Hashable { 87 | let table: String 88 | fileprivate let lookupKey: String 89 | 90 | init(table: String, lookupKey: String) { 91 | self.table = table 92 | self.lookupKey = lookupKey 93 | } 94 | 95 | var key: LocalizedStringKey { 96 | LocalizedStringKey(lookupKey) 97 | } 98 | 99 | var text: String { 100 | tr(table, lookupKey) 101 | } 102 | 103 | func text(withLocale locale: Locale) -> String { 104 | tr(table, lookupKey, locale) 105 | } 106 | } 107 | {% endif %} 108 | {% if not param.bundle %} 109 | 110 | private final class BundleToken { 111 | static let bundle: Bundle = { 112 | #if SWIFT_PACKAGE 113 | return Bundle.module 114 | #else 115 | return Bundle(for: BundleToken.self) 116 | #endif 117 | }() 118 | } 119 | {% endif %} 120 | {% else %} 121 | // No string found 122 | {% endif %} 123 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Generated/strings.swift: -------------------------------------------------------------------------------- 1 | // Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen 2 | // Derived from https://github.com/SwiftGen/SwiftGen/issues/685#issuecomment-782893242 3 | // Origin see https://github.com/SwiftGen/SwiftGen/blob/stable/templates/strings/structured-swift5.stencil 4 | // 5 | // Example: 6 | // 7 | // var body: some View { 8 | // VStack { 9 | // Text(L10n.Login.PageTitle.header.key) // return LocalizedStringKey 10 | // CustomView(string: L10n.Login.Step1.title.text) // return String 11 | // } 12 | // } 13 | 14 | import SwiftUI 15 | 16 | // MARK: - Strings 17 | 18 | public enum L10n { 19 | /// WeChat 20 | public static let appName = LocalizedString(table: "Localizable", lookupKey: "AppName") 21 | public enum Discover { 22 | /// Channels 23 | public static let channels = LocalizedString(table: "Localizable", lookupKey: "Discover.Channels") 24 | /// Live 25 | public static let live = LocalizedString(table: "Localizable", lookupKey: "Discover.Live") 26 | /// Mini Programs 27 | public static let miniPrograms = LocalizedString(table: "Localizable", lookupKey: "Discover.MiniPrograms") 28 | /// Moments 29 | public static let moments = LocalizedString(table: "Localizable", lookupKey: "Discover.Moments") 30 | /// Nearby 31 | public static let nearby = LocalizedString(table: "Localizable", lookupKey: "Discover.Nearby") 32 | /// Scan 33 | public static let scan = LocalizedString(table: "Localizable", lookupKey: "Discover.Scan") 34 | /// Search 35 | public static let search = LocalizedString(table: "Localizable", lookupKey: "Discover.Search") 36 | /// Shake 37 | public static let shake = LocalizedString(table: "Localizable", lookupKey: "Discover.Shake") 38 | /// Top Stories 39 | public static let topStories = LocalizedString(table: "Localizable", lookupKey: "Discover.TopStories") 40 | public enum Live { 41 | /// Live 42 | public static let onAir = LocalizedString(table: "Localizable", lookupKey: "Discover.Live.OnAir") 43 | } 44 | } 45 | public enum Me { 46 | /// Cards & Offers 47 | public static let cards = LocalizedString(table: "Localizable", lookupKey: "Me.Cards") 48 | /// Channels 49 | public static let channels = LocalizedString(table: "Localizable", lookupKey: "Me.Channels") 50 | /// Favorites 51 | public static let favorites = LocalizedString(table: "Localizable", lookupKey: "Me.Favorites") 52 | /// Moments 53 | public static let moments = LocalizedString(table: "Localizable", lookupKey: "Me.Moments") 54 | /// Pay 55 | public static let pay = LocalizedString(table: "Localizable", lookupKey: "Me.Pay") 56 | /// Settings 57 | public static let settings = LocalizedString(table: "Localizable", lookupKey: "Me.Settings") 58 | /// Sticker Gallery 59 | public static let sticker = LocalizedString(table: "Localizable", lookupKey: "Me.Sticker") 60 | } 61 | public enum MyProfile { 62 | /// Wavky Huang 63 | public static let name = LocalizedString(table: "Localizable", lookupKey: "MyProfile.Name") 64 | /// Status 65 | public static let status = LocalizedString(table: "Localizable", lookupKey: "MyProfile.Status") 66 | /// WeChat ID: 67 | public static let weChatID = LocalizedString(table: "Localizable", lookupKey: "MyProfile.WeChatID") 68 | } 69 | public enum QrCode { 70 | /// My QR Code 71 | public static let title = LocalizedString(table: "Localizable", lookupKey: "QrCode.Title") 72 | } 73 | public enum SearchBar { 74 | /// Search 75 | public static let hint = LocalizedString(table: "Localizable", lookupKey: "SearchBar.Hint") 76 | } 77 | public enum Tabs { 78 | /// Chats 79 | public static let chats = LocalizedString(table: "Localizable", lookupKey: "Tabs.Chats") 80 | /// Contacts 81 | public static let contacts = LocalizedString(table: "Localizable", lookupKey: "Tabs.Contacts") 82 | /// Discover 83 | public static let discover = LocalizedString(table: "Localizable", lookupKey: "Tabs.Discover") 84 | /// Me 85 | public static let me = LocalizedString(table: "Localizable", lookupKey: "Tabs.Me") 86 | } 87 | } 88 | 89 | // MARK: - Implementation Details 90 | fileprivate func tr(_ table: String, _ key: String, _ locale: Locale = Locale.current, _ args: CVarArg...) -> String { 91 | let path = Bundle.main.path(forResource: locale.identifier, ofType: "lproj") ?? "" 92 | let format: String 93 | if let bundle = Bundle(path: path) { 94 | format = NSLocalizedString(key, tableName: table, bundle: bundle, comment: "") 95 | } else { 96 | format = BundleToken.bundle.localizedString(forKey: key, value: nil, table: table) 97 | } 98 | return String(format: format, locale: locale, arguments: args) 99 | } 100 | 101 | public struct LocalizedString: Hashable { 102 | let table: String 103 | fileprivate let lookupKey: String 104 | 105 | init(table: String, lookupKey: String) { 106 | self.table = table 107 | self.lookupKey = lookupKey 108 | } 109 | 110 | var key: LocalizedStringKey { 111 | LocalizedStringKey(lookupKey) 112 | } 113 | 114 | var text: String { 115 | tr(table, lookupKey) 116 | } 117 | 118 | func text(withLocale locale: Locale) -> String { 119 | tr(table, lookupKey, locale) 120 | } 121 | } 122 | 123 | private final class BundleToken { 124 | static let bundle: Bundle = { 125 | #if SWIFT_PACKAGE 126 | return Bundle.module 127 | #else 128 | return Bundle(for: BundleToken.self) 129 | #endif 130 | }() 131 | } 132 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/UI/Common/ItemBarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemBarView.swift 3 | // SwiftUI-WeChatDemo 4 | // 5 | // Created by Wavky Huang on 2021/07/16. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ItemBarView: View { 11 | let itemBarInfo: ItemBarInfo 12 | let withDivider: Bool 13 | let withBudge: Bool 14 | 15 | var body: some View { 16 | Button(action: {}) { 17 | HStack { 18 | Icon(image: itemBarInfo.icon, pattern: itemBarInfo.iconPattern) 19 | .resizeAsIcon() 20 | Spacer(minLength: 15) 21 | 22 | VStack(spacing:0) { 23 | Spacer(minLength: 0) 24 | HStack { 25 | Text(itemBarInfo.title) 26 | .fontWeight(.light) 27 | .font(.title3) 28 | .lineLimit(1) 29 | .frame(alignment: .leading) 30 | Spacer() 31 | if let name = itemBarInfo.name { 32 | Text(name.key, tableName: name.table) 33 | .font(.callout) 34 | .fontWeight(.light) 35 | .lineLimit(1) 36 | .foregroundColor(.gray) 37 | .frame(alignment: .leading) 38 | } 39 | if let profileImage = itemBarInfo.profileImage { 40 | Image(profileImage) 41 | .resizable() 42 | .scaledToFit() 43 | .frame(width: 40, height: 40, alignment: .center) 44 | .clipShape(Circle()) 45 | .withBadge(withBudge, radius: 5) 46 | } 47 | Image(systemName: "chevron.forward") 48 | .resizable() 49 | .scaledToFit() 50 | .frame(width: 18, height: 18, alignment: .center) 51 | .foregroundColor(Asset.Color.lightGray.color) 52 | } 53 | .padding(.vertical, 12) 54 | Spacer(minLength: 0) 55 | if withDivider { 56 | Divider() 57 | } 58 | } 59 | } 60 | } 61 | .accentColor(.black) 62 | .padding(.horizontal) 63 | .background(Color.white) 64 | .frame(height: 65) 65 | .frame(maxWidth: .infinity) 66 | } 67 | } 68 | 69 | private struct Icon: View { 70 | let image: String 71 | let color = Color.red 72 | let pattern: IconPattern? 73 | 74 | var body: some View { 75 | switch pattern { 76 | case .Color(let color): 77 | return AnyView(color.mask(Image(image).fitSize())) 78 | case .AngularGradient(let gradient): 79 | return AnyView(gradient.mask(Image(image).fitSize())) 80 | case .LinearGradient(let gradient): 81 | return AnyView(gradient.mask(Image(image).fitSize())) 82 | case .RadialGradient(let gradient): 83 | return AnyView(gradient.mask(Image(image).fitSize())) 84 | default: 85 | return AnyView(Image(image).fitSize()) 86 | } 87 | } 88 | } 89 | 90 | private extension Image { 91 | func fitSize() -> some View { 92 | return self.resizable() 93 | .scaledToFit() 94 | } 95 | } 96 | 97 | private extension View { 98 | func resizeAsIcon() -> some View { 99 | self.frame(width: 30, height: 30, alignment: .center) 100 | } 101 | } 102 | 103 | struct ItemBarInfo { 104 | let icon: String 105 | let iconPattern: IconPattern? 106 | let title: LocalizedStringKey 107 | let name: LocalizedString? 108 | let profileImage: String? 109 | } 110 | 111 | extension ItemBarInfo { 112 | func toItemBarView(withDivider: Bool = false, withBudge: Bool = true) -> ItemBarView { 113 | ItemBarView(itemBarInfo: self, withDivider: withDivider, withBudge: withBudge) 114 | } 115 | } 116 | 117 | enum IconPattern { 118 | case Color(Color) 119 | case AngularGradient(AngularGradient) 120 | case LinearGradient(LinearGradient) 121 | case RadialGradient(RadialGradient) 122 | } 123 | 124 | struct ItemBarView_Previews: PreviewProvider { 125 | static let contacts = getDefaultContactList().shuffled() 126 | static let itemBarInfos = [ 127 | ItemBarInfo(icon: Asset.Image.discoverMoments.name, iconPattern: IconPattern.AngularGradient(AngularGradient(gradient: Gradient(colors: [Asset.Color.momentsBlue.color, Asset.Color.momentsGreen.color, Asset.Color.momentsYellow.color, Asset.Color.momentsRed.color, Asset.Color.momentsBlue.color]), center: .center)), title: "Moments", name: contacts[0].name, profileImage: contacts[0].profileImage), 128 | ItemBarInfo(icon: Asset.Image.discoverChannels.name, iconPattern: IconPattern.Color(Asset.Color.channelsOrange.color), title: "Channels", name: nil, profileImage: contacts[1].profileImage), 129 | ItemBarInfo(icon: Asset.Image.discoverLive.name, iconPattern: IconPattern.Color(Asset.Color.liveRed.color), title: "Live", name: nil, profileImage: contacts[2].profileImage) 130 | ] 131 | static var previews: some View { 132 | VStack(spacing: 0) { 133 | ForEach(0..<3, id: \.self) { index in 134 | ItemBarView(itemBarInfo: itemBarInfos[index], withDivider: true, withBudge: true) 135 | } 136 | Spacer() 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /SwiftUI-WeChatDemo/Generated/assets.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | // Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen 3 | 4 | #if os(macOS) 5 | import AppKit 6 | #elseif os(iOS) 7 | import UIKit 8 | #elseif os(tvOS) || os(watchOS) 9 | import UIKit 10 | #endif 11 | import SwiftUI 12 | 13 | // Deprecated typealiases 14 | @available(*, deprecated, renamed: "ColorAsset.Color", message: "This typealias will be removed in SwiftGen 7.0") 15 | internal typealias AssetColorTypeAlias = ColorAsset.Color 16 | @available(*, deprecated, renamed: "ImageAsset.Image", message: "This typealias will be removed in SwiftGen 7.0") 17 | internal typealias AssetImageTypeAlias = ImageAsset.Image 18 | 19 | // swiftlint:disable superfluous_disable_command file_length implicit_return 20 | 21 | // MARK: - Asset Catalogs 22 | 23 | // swiftlint:disable identifier_name line_length nesting type_body_length type_name 24 | internal enum Asset { 25 | internal enum Color { 26 | internal static let accentColor = ColorAsset(name: "AccentColor") 27 | internal static let backgroudGray = ColorAsset(name: "BackgroudGray") 28 | internal static let chatGray = ColorAsset(name: "ChatGray") 29 | internal static let chatGreen = ColorAsset(name: "ChatGreen") 30 | internal static let channelsOrange = ColorAsset(name: "ChannelsOrange") 31 | internal static let liveRed = ColorAsset(name: "LiveRed") 32 | internal static let miniProgramsPurple = ColorAsset(name: "MiniProgramsPurple") 33 | internal static let momentsBlue = ColorAsset(name: "moments.blue") 34 | internal static let momentsGreen = ColorAsset(name: "moments.green") 35 | internal static let momentsRed = ColorAsset(name: "moments.red") 36 | internal static let momentsYellow = ColorAsset(name: "moments.yellow") 37 | internal static let nearbyBlue = ColorAsset(name: "NearbyBlue") 38 | internal static let scanBlue = ColorAsset(name: "ScanBlue") 39 | internal static let searchRed = ColorAsset(name: "SearchRed") 40 | internal static let shakeBlue = ColorAsset(name: "ShakeBlue") 41 | internal static let topStoriesYellow = ColorAsset(name: "TopStoriesYellow") 42 | internal static let lightGray = ColorAsset(name: "LightGray") 43 | internal static let cardsBlue = ColorAsset(name: "CardsBlue") 44 | internal static let favoritesBlue = ColorAsset(name: "favorites.blue") 45 | internal static let favoritesRed = ColorAsset(name: "favorites.red") 46 | internal static let favoritesYellow = ColorAsset(name: "favorites.yellow") 47 | internal static let meMomentsBlue = ColorAsset(name: "MeMomentsBlue") 48 | internal static let payGreen = ColorAsset(name: "PayGreen") 49 | internal static let settingsBlue = ColorAsset(name: "SettingsBlue") 50 | internal static let stickerYellow = ColorAsset(name: "StickerYellow") 51 | internal static let searchBarBackgroundGray = ColorAsset(name: "SearchBarBackgroundGray") 52 | internal static let searchGray = ColorAsset(name: "SearchGray") 53 | internal static let tabbarGray = ColorAsset(name: "TabbarGray") 54 | internal static let topBarGray = ColorAsset(name: "TopBarGray") 55 | } 56 | internal enum Image { 57 | internal static let splash = ImageAsset(name: "Splash") 58 | internal static let avatar01 = ImageAsset(name: "avatar01") 59 | internal static let avatar02 = ImageAsset(name: "avatar02") 60 | internal static let avatar03 = ImageAsset(name: "avatar03") 61 | internal static let avatar04 = ImageAsset(name: "avatar04") 62 | internal static let avatar05 = ImageAsset(name: "avatar05") 63 | internal static let avatar06 = ImageAsset(name: "avatar06") 64 | internal static let avatar07 = ImageAsset(name: "avatar07") 65 | internal static let avatar08 = ImageAsset(name: "avatar08") 66 | internal static let avatar09 = ImageAsset(name: "avatar09") 67 | internal static let avatar10 = ImageAsset(name: "avatar10") 68 | internal static let avatarMe = ImageAsset(name: "avatar_me") 69 | internal static let discoverChannels = ImageAsset(name: "discover.channels") 70 | internal static let discoverLive = ImageAsset(name: "discover.live") 71 | internal static let discoverMiniPrograms = ImageAsset(name: "discover.miniPrograms") 72 | internal static let discoverMoments = ImageAsset(name: "discover.moments") 73 | internal static let discoverNearby = ImageAsset(name: "discover.nearby") 74 | internal static let discoverScan = ImageAsset(name: "discover.scan") 75 | internal static let discoverSearch = ImageAsset(name: "discover.search") 76 | internal static let discoverShake = ImageAsset(name: "discover.shake") 77 | internal static let discoverTopStories = ImageAsset(name: "discover.topStories") 78 | internal static let meCards = ImageAsset(name: "me.cards") 79 | internal static let meChannels = ImageAsset(name: "me.channels") 80 | internal static let meFavorites = ImageAsset(name: "me.favorites") 81 | internal static let meMoments = ImageAsset(name: "me.moments") 82 | internal static let mePay = ImageAsset(name: "me.pay") 83 | internal static let meSettings = ImageAsset(name: "me.settings") 84 | internal static let meSticker = ImageAsset(name: "me.sticker") 85 | internal static let myprofileQrcodeIcon = ImageAsset(name: "myprofile.qrcode.icon") 86 | internal static let myprofileQrcode = ImageAsset(name: "myprofile.qrcode") 87 | } 88 | } 89 | // swiftlint:enable identifier_name line_length nesting type_body_length type_name 90 | 91 | // MARK: - Implementation Details 92 | 93 | internal final class ColorAsset { 94 | internal fileprivate(set) var name: String 95 | 96 | #if os(macOS) 97 | internal typealias Color = NSColor 98 | #elseif os(iOS) || os(tvOS) || os(watchOS) 99 | internal typealias Color = UIColor 100 | #endif 101 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 102 | internal private(set) lazy var color: SwiftUI.Color = { SwiftUI.Color(name) }() 103 | 104 | @available(macOS 10.13, *) 105 | @available(iOS, unavailable) 106 | @available(tvOS, unavailable) 107 | @available(watchOS, unavailable) 108 | internal private(set) lazy var nsColor: Color = { 109 | guard let color = Color(asset: self) else { 110 | fatalError("Unable to load color asset named \(name).") 111 | } 112 | return color 113 | }() 114 | 115 | @available(macOS, unavailable) 116 | @available(iOS 11.0, tvOS 11.0, watchOS 4.0, *) 117 | internal private(set) lazy var uiColor: Color = { 118 | guard let color = Color(asset: self) else { 119 | fatalError("Unable to load color asset named \(name).") 120 | } 121 | return color 122 | }() 123 | 124 | fileprivate init(name: String) { 125 | self.name = name 126 | } 127 | } 128 | 129 | internal extension ColorAsset.Color { 130 | @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *) 131 | convenience init?(asset: ColorAsset) { 132 | let bundle = BundleToken.bundle 133 | #if os(iOS) || os(tvOS) 134 | self.init(named: asset.name, in: bundle, compatibleWith: nil) 135 | #elseif os(macOS) 136 | self.init(named: NSColor.Name(asset.name), bundle: bundle) 137 | #elseif os(watchOS) 138 | self.init(named: asset.name) 139 | #endif 140 | } 141 | } 142 | 143 | internal struct ImageAsset { 144 | internal fileprivate(set) var name: String 145 | 146 | #if os(macOS) 147 | internal typealias Image = NSImage 148 | #elseif os(iOS) || os(tvOS) || os(watchOS) 149 | internal typealias Image = UIImage 150 | #endif 151 | 152 | internal var image: Image { 153 | let bundle = BundleToken.bundle 154 | #if os(iOS) || os(tvOS) 155 | let image = Image(named: name, in: bundle, compatibleWith: nil) 156 | #elseif os(macOS) 157 | let name = NSImage.Name(self.name) 158 | let image = (bundle == .main) ? NSImage(named: name) : bundle.image(forResource: name) 159 | #elseif os(watchOS) 160 | let image = Image(named: name) 161 | #endif 162 | guard let result = image else { 163 | fatalError("Unable to load image asset named \(name).") 164 | } 165 | return result 166 | } 167 | } 168 | 169 | internal extension ImageAsset.Image { 170 | @available(macOS, deprecated, 171 | message: "This initializer is unsafe on macOS, please use the ImageAsset.image property") 172 | convenience init?(asset: ImageAsset) { 173 | #if os(iOS) || os(tvOS) 174 | let bundle = BundleToken.bundle 175 | self.init(named: asset.name, in: bundle, compatibleWith: nil) 176 | #elseif os(macOS) 177 | self.init(named: NSImage.Name(asset.name)) 178 | #elseif os(watchOS) 179 | self.init(named: asset.name) 180 | #endif 181 | } 182 | } 183 | 184 | // swiftlint:disable convenience_type 185 | private final class BundleToken { 186 | static let bundle: Bundle = { 187 | #if SWIFT_PACKAGE 188 | return Bundle.module 189 | #else 190 | return Bundle(for: BundleToken.self) 191 | #endif 192 | }() 193 | } 194 | // swiftlint:enable convenience_type 195 | -------------------------------------------------------------------------------- /swiftui-assets-template.stencil: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | // Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen 3 | 4 | {% if catalogs %} 5 | {% set enumName %}{{param.enumName|default:"Asset"}}{% endset %} 6 | {% set arResourceGroupType %}{{param.arResourceGroupTypeName|default:"ARResourceGroupAsset"}}{% endset %} 7 | {% set colorType %}{{param.colorTypeName|default:"ColorAsset"}}{% endset %} 8 | {% set dataType %}{{param.dataTypeName|default:"DataAsset"}}{% endset %} 9 | {% set imageType %}{{param.imageTypeName|default:"ImageAsset"}}{% endset %} 10 | {% set forceNamespaces %}{{param.forceProvidesNamespaces|default:"false"}}{% endset %} 11 | {% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %} 12 | #if os(macOS) 13 | import AppKit 14 | #elseif os(iOS) 15 | {% if resourceCount.arresourcegroup > 0 %} 16 | import ARKit 17 | {% endif %} 18 | import UIKit 19 | #elseif os(tvOS) || os(watchOS) 20 | import UIKit 21 | #endif 22 | import SwiftUI 23 | 24 | // Deprecated typealiases 25 | {% if resourceCount.color > 0 %} 26 | @available(*, deprecated, renamed: "{{colorType}}.Color", message: "This typealias will be removed in SwiftGen 7.0") 27 | {{accessModifier}} typealias {{param.colorAliasName|default:"AssetColorTypeAlias"}} = {{colorType}}.Color 28 | {% endif %} 29 | {% if resourceCount.image > 0 %} 30 | @available(*, deprecated, renamed: "{{imageType}}.Image", message: "This typealias will be removed in SwiftGen 7.0") 31 | {{accessModifier}} typealias {{param.imageAliasName|default:"AssetImageTypeAlias"}} = {{imageType}}.Image 32 | {% endif %} 33 | 34 | // swiftlint:disable superfluous_disable_command file_length implicit_return 35 | 36 | // MARK: - Asset Catalogs 37 | 38 | {% macro enumBlock assets %} 39 | {% call casesBlock assets %} 40 | {% if param.allValues %} 41 | 42 | // swiftlint:disable trailing_comma 43 | {% if resourceCount.arresourcegroup > 0 %} 44 | {{accessModifier}} static let allResourceGroups: [{{arResourceGroupType}}] = [ 45 | {% filter indent:2 %}{% call allValuesBlock assets "arresourcegroup" "" %}{% endfilter %} 46 | ] 47 | {% endif %} 48 | {% if resourceCount.color > 0 %} 49 | {{accessModifier}} static let allColors: [{{colorType}}] = [ 50 | {% filter indent:2 %}{% call allValuesBlock assets "color" "" %}{% endfilter %} 51 | ] 52 | {% endif %} 53 | {% if resourceCount.data > 0 %} 54 | {{accessModifier}} static let allDataAssets: [{{dataType}}] = [ 55 | {% filter indent:2 %}{% call allValuesBlock assets "data" "" %}{% endfilter %} 56 | ] 57 | {% endif %} 58 | {% if resourceCount.image > 0 %} 59 | {{accessModifier}} static let allImages: [{{imageType}}] = [ 60 | {% filter indent:2 %}{% call allValuesBlock assets "image" "" %}{% endfilter %} 61 | ] 62 | {% endif %} 63 | // swiftlint:enable trailing_comma 64 | {% endif %} 65 | {% endmacro %} 66 | {% macro casesBlock assets %} 67 | {% for asset in assets %} 68 | {% if asset.type == "arresourcegroup" %} 69 | {{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{arResourceGroupType}}(name: "{{asset.value}}") 70 | {% elif asset.type == "color" %} 71 | {{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{colorType}}(name: "{{asset.value}}") 72 | {% elif asset.type == "data" %} 73 | {{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{dataType}}(name: "{{asset.value}}") 74 | {% elif asset.type == "image" %} 75 | {{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{imageType}}(name: "{{asset.value}}") 76 | {% elif asset.items and ( forceNamespaces == "true" or asset.isNamespaced == "true" ) %} 77 | {{accessModifier}} enum {{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { 78 | {% filter indent:2 %}{% call casesBlock asset.items %}{% endfilter %} 79 | } 80 | {% elif asset.items %} 81 | {% call casesBlock asset.items %} 82 | {% endif %} 83 | {% endfor %} 84 | {% endmacro %} 85 | {% macro allValuesBlock assets filter prefix %} 86 | {% for asset in assets %} 87 | {% if asset.type == filter %} 88 | {{prefix}}{{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}, 89 | {% elif asset.items and ( forceNamespaces == "true" or asset.isNamespaced == "true" ) %} 90 | {% set prefix2 %}{{prefix}}{{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}.{% endset %} 91 | {% call allValuesBlock asset.items filter prefix2 %} 92 | {% elif asset.items %} 93 | {% call allValuesBlock asset.items filter prefix %} 94 | {% endif %} 95 | {% endfor %} 96 | {% endmacro %} 97 | // swiftlint:disable identifier_name line_length nesting type_body_length type_name 98 | {{accessModifier}} enum {{enumName}} { 99 | {% if catalogs.count > 1 or param.forceFileNameEnum %} 100 | {% for catalog in catalogs %} 101 | {{accessModifier}} enum {{catalog.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { 102 | {% filter indent:2 %}{% call enumBlock catalog.assets %}{% endfilter %} 103 | } 104 | {% endfor %} 105 | {% else %} 106 | {% call enumBlock catalogs.first.assets %} 107 | {% endif %} 108 | } 109 | // swiftlint:enable identifier_name line_length nesting type_body_length type_name 110 | 111 | // MARK: - Implementation Details 112 | {% if resourceCount.arresourcegroup > 0 %} 113 | 114 | {{accessModifier}} struct {{arResourceGroupType}} { 115 | {{accessModifier}} fileprivate(set) var name: String 116 | 117 | #if os(iOS) 118 | @available(iOS 11.3, *) 119 | {{accessModifier}} var referenceImages: Set { 120 | return ARReferenceImage.referenceImages(in: self) 121 | } 122 | 123 | @available(iOS 12.0, *) 124 | {{accessModifier}} var referenceObjects: Set { 125 | return ARReferenceObject.referenceObjects(in: self) 126 | } 127 | #endif 128 | } 129 | 130 | #if os(iOS) 131 | @available(iOS 11.3, *) 132 | {{accessModifier}} extension ARReferenceImage { 133 | static func referenceImages(in asset: {{arResourceGroupType}}) -> Set { 134 | let bundle = {{param.bundle|default:"BundleToken.bundle"}} 135 | return referenceImages(inGroupNamed: asset.name, bundle: bundle) ?? Set() 136 | } 137 | } 138 | 139 | @available(iOS 12.0, *) 140 | {{accessModifier}} extension ARReferenceObject { 141 | static func referenceObjects(in asset: {{arResourceGroupType}}) -> Set { 142 | let bundle = {{param.bundle|default:"BundleToken.bundle"}} 143 | return referenceObjects(inGroupNamed: asset.name, bundle: bundle) ?? Set() 144 | } 145 | } 146 | #endif 147 | {% endif %} 148 | {% if resourceCount.color > 0 %} 149 | 150 | {{accessModifier}} final class {{colorType}} { 151 | {{accessModifier}} fileprivate(set) var name: String 152 | 153 | #if os(macOS) 154 | {{accessModifier}} typealias Color = NSColor 155 | #elseif os(iOS) || os(tvOS) || os(watchOS) 156 | {{accessModifier}} typealias Color = UIColor 157 | #endif 158 | 159 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 160 | internal private(set) lazy var color: SwiftUI.Color = { SwiftUI.Color(name) }() 161 | 162 | @available(macOS 10.13, *) 163 | @available(iOS, unavailable) 164 | @available(tvOS, unavailable) 165 | @available(watchOS, unavailable) 166 | internal private(set) lazy var nsColor: Color = { 167 | guard let color = Color(asset: self) else { 168 | fatalError("Unable to load color asset named \(name).") 169 | } 170 | return color 171 | }() 172 | 173 | @available(macOS, unavailable) 174 | @available(iOS 11.0, tvOS 11.0, watchOS 4.0, *) 175 | internal private(set) lazy var uiColor: Color = { 176 | guard let color = Color(asset: self) else { 177 | fatalError("Unable to load color asset named \(name).") 178 | } 179 | return color 180 | }() 181 | 182 | fileprivate init(name: String) { 183 | self.name = name 184 | } 185 | } 186 | 187 | {{accessModifier}} extension {{colorType}}.Color { 188 | @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *) 189 | convenience init?(asset: {{colorType}}) { 190 | let bundle = {{param.bundle|default:"BundleToken.bundle"}} 191 | #if os(iOS) || os(tvOS) 192 | self.init(named: asset.name, in: bundle, compatibleWith: nil) 193 | #elseif os(macOS) 194 | self.init(named: NSColor.Name(asset.name), bundle: bundle) 195 | #elseif os(watchOS) 196 | self.init(named: asset.name) 197 | #endif 198 | } 199 | } 200 | {% endif %} 201 | {% if resourceCount.data > 0 %} 202 | 203 | {{accessModifier}} struct {{dataType}} { 204 | {{accessModifier}} fileprivate(set) var name: String 205 | 206 | #if os(iOS) || os(tvOS) || os(macOS) 207 | @available(iOS 9.0, macOS 10.11, *) 208 | {{accessModifier}} var data: NSDataAsset { 209 | guard let data = NSDataAsset(asset: self) else { 210 | fatalError("Unable to load data asset named \(name).") 211 | } 212 | return data 213 | } 214 | #endif 215 | } 216 | 217 | #if os(iOS) || os(tvOS) || os(macOS) 218 | @available(iOS 9.0, macOS 10.11, *) 219 | {{accessModifier}} extension NSDataAsset { 220 | convenience init?(asset: {{dataType}}) { 221 | let bundle = {{param.bundle|default:"BundleToken.bundle"}} 222 | #if os(iOS) || os(tvOS) 223 | self.init(name: asset.name, bundle: bundle) 224 | #elseif os(macOS) 225 | self.init(name: NSDataAsset.Name(asset.name), bundle: bundle) 226 | #endif 227 | } 228 | } 229 | #endif 230 | {% endif %} 231 | {% if resourceCount.image > 0 %} 232 | 233 | {{accessModifier}} struct {{imageType}} { 234 | {{accessModifier}} fileprivate(set) var name: String 235 | 236 | #if os(macOS) 237 | {{accessModifier}} typealias Image = NSImage 238 | #elseif os(iOS) || os(tvOS) || os(watchOS) 239 | {{accessModifier}} typealias Image = UIImage 240 | #endif 241 | 242 | {{accessModifier}} var image: Image { 243 | let bundle = {{param.bundle|default:"BundleToken.bundle"}} 244 | #if os(iOS) || os(tvOS) 245 | let image = Image(named: name, in: bundle, compatibleWith: nil) 246 | #elseif os(macOS) 247 | let name = NSImage.Name(self.name) 248 | let image = (bundle == .main) ? NSImage(named: name) : bundle.image(forResource: name) 249 | #elseif os(watchOS) 250 | let image = Image(named: name) 251 | #endif 252 | guard let result = image else { 253 | fatalError("Unable to load image asset named \(name).") 254 | } 255 | return result 256 | } 257 | } 258 | 259 | {{accessModifier}} extension {{imageType}}.Image { 260 | @available(macOS, deprecated, 261 | message: "This initializer is unsafe on macOS, please use the {{imageType}}.image property") 262 | convenience init?(asset: {{imageType}}) { 263 | #if os(iOS) || os(tvOS) 264 | let bundle = {{param.bundle|default:"BundleToken.bundle"}} 265 | self.init(named: asset.name, in: bundle, compatibleWith: nil) 266 | #elseif os(macOS) 267 | self.init(named: NSImage.Name(asset.name)) 268 | #elseif os(watchOS) 269 | self.init(named: asset.name) 270 | #endif 271 | } 272 | } 273 | {% endif %} 274 | {% if not param.bundle %} 275 | 276 | // swiftlint:disable convenience_type 277 | private final class BundleToken { 278 | static let bundle: Bundle = { 279 | #if SWIFT_PACKAGE 280 | return Bundle.module 281 | #else 282 | return Bundle(for: BundleToken.self) 283 | #endif 284 | }() 285 | } 286 | // swiftlint:enable convenience_type 287 | {% endif %} 288 | {% else %} 289 | // No assets found 290 | {% endif %} 291 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------