From f9fced19e69383c9e92393fa1c6bc752c70cf5f2 Mon Sep 17 00:00:00 2001 From: Enes Karaosman Date: Sat, 13 Apr 2024 15:39:32 +0300 Subject: [PATCH] refactor: move mock related files to newly created SwiftyChatMock package (#63) * refactor: move mock related files into SwiftyChatMock target * Update README.md * Update Lorem.swift * Update MessageMocker.swift * Update AdvancedExampleView.swift --- Example/Example.xcodeproj/project.pbxproj | 20 +- Example/Example/AdvancedExampleView.swift | 18 +- Example/Example/BasicExampleView.swift | 7 +- Package.swift | 16 +- README.md | 3 + .../MessageViews/LoadingMessageView.swift | 9 - .../Mock => SwiftyChatMock}/Lorem.swift | 26 +- .../MessageMocker.swift} | 271 +++++++++--------- 8 files changed, 184 insertions(+), 186 deletions(-) rename Sources/{SwiftyChat/Mock => SwiftyChatMock}/Lorem.swift (92%) rename Sources/{SwiftyChat/Mock/MockMessages.swift => SwiftyChatMock/MessageMocker.swift} (73%) diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index a1286b45..7259c0e7 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -10,11 +10,12 @@ 94410B752BC6B4D400C37BDB /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94410B742BC6B4D400C37BDB /* ExampleApp.swift */; }; 94410B792BC6B4D700C37BDB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 94410B782BC6B4D700C37BDB /* Assets.xcassets */; }; 94410B7C2BC6B4D700C37BDB /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 94410B7B2BC6B4D700C37BDB /* Preview Assets.xcassets */; }; - 94410B842BC6B50700C37BDB /* SwiftyChat in Frameworks */ = {isa = PBXBuildFile; productRef = 94410B832BC6B50700C37BDB /* SwiftyChat */; }; 94410B862BC6B53900C37BDB /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94410B852BC6B53900C37BDB /* ChatListView.swift */; }; 94410B882BC6B53E00C37BDB /* AdvancedExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94410B872BC6B53E00C37BDB /* AdvancedExampleView.swift */; }; 94410B8A2BC6B54100C37BDB /* BasicExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94410B892BC6B54100C37BDB /* BasicExampleView.swift */; }; 94410B8C2BC6B54900C37BDB /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94410B8B2BC6B54900C37BDB /* Extensions.swift */; }; + 9498B4232BCAADBB0021FB93 /* SwiftyChat in Frameworks */ = {isa = PBXBuildFile; productRef = 9498B4222BCAADBB0021FB93 /* SwiftyChat */; }; + 9498B4252BCAADBB0021FB93 /* SwiftyChatMock in Frameworks */ = {isa = PBXBuildFile; productRef = 9498B4242BCAADBB0021FB93 /* SwiftyChatMock */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -34,7 +35,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 94410B842BC6B50700C37BDB /* SwiftyChat in Frameworks */, + 9498B4252BCAADBB0021FB93 /* SwiftyChatMock in Frameworks */, + 9498B4232BCAADBB0021FB93 /* SwiftyChat in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -46,6 +48,7 @@ children = ( 94410B732BC6B4D400C37BDB /* Example */, 94410B722BC6B4D400C37BDB /* Products */, + 9498B4192BC982FF0021FB93 /* Frameworks */, ); sourceTree = ""; }; @@ -97,7 +100,8 @@ ); name = Example; packageProductDependencies = ( - 94410B832BC6B50700C37BDB /* SwiftyChat */, + 9498B4222BCAADBB0021FB93 /* SwiftyChat */, + 9498B4242BCAADBB0021FB93 /* SwiftyChatMock */, ); productName = Example; productReference = 94410B712BC6B4D400C37BDB /* Example.app */; @@ -128,7 +132,7 @@ ); mainGroup = 94410B682BC6B4D400C37BDB; packageReferences = ( - 94410B822BC6B50700C37BDB /* XCLocalSwiftPackageReference ".." */, + 9498B4212BCAADBB0021FB93 /* XCLocalSwiftPackageReference ".." */, ); productRefGroup = 94410B722BC6B4D400C37BDB /* Products */; projectDirPath = ""; @@ -374,17 +378,21 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 94410B822BC6B50700C37BDB /* XCLocalSwiftPackageReference ".." */ = { + 9498B4212BCAADBB0021FB93 /* XCLocalSwiftPackageReference ".." */ = { isa = XCLocalSwiftPackageReference; relativePath = ..; }; /* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 94410B832BC6B50700C37BDB /* SwiftyChat */ = { + 9498B4222BCAADBB0021FB93 /* SwiftyChat */ = { isa = XCSwiftPackageProductDependency; productName = SwiftyChat; }; + 9498B4242BCAADBB0021FB93 /* SwiftyChatMock */ = { + isa = XCSwiftPackageProductDependency; + productName = SwiftyChatMock; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 94410B692BC6B4D400C37BDB /* Project object */; diff --git a/Example/Example/AdvancedExampleView.swift b/Example/Example/AdvancedExampleView.swift index c845b0d8..7a94ff86 100644 --- a/Example/Example/AdvancedExampleView.swift +++ b/Example/Example/AdvancedExampleView.swift @@ -7,10 +7,11 @@ import SwiftUI import SwiftyChat +import SwiftyChatMock struct AdvancedExampleView: View { - @State var messages: [MockMessages.ChatMessageItem] = [] + @State var messages: [MessageMocker.ChatMessageItem] = [] @State private var scrollToBottom = false @State private var message = "" @@ -20,13 +21,13 @@ struct AdvancedExampleView: View { } private var chatView: some View { - ChatView(messages: $messages) { + ChatView(messages: $messages, scrollToBottom: $scrollToBottom) { BasicInputView( message: $message, placeholder: "Type something", onCommit: { messageKind in self.messages.append( - .init(user: MockMessages.sender, messageKind: messageKind, isSender: true) + .init(user: MessageMocker.sender, messageKind: messageKind, isSender: true) ) } ) @@ -59,8 +60,8 @@ struct AdvancedExampleView: View { // ▼ Implement in case ChatMessageKind.quickReply .onQuickReplyItemSelected { (quickReply) in self.messages.append( - MockMessages.ChatMessageItem( - user: MockMessages.sender, + MessageMocker.ChatMessageItem( + user: MessageMocker.sender, messageKind: .text(quickReply.title), isSender: true ) @@ -83,20 +84,19 @@ struct AdvancedExampleView: View { #if os(iOS) .navigationBarTitle("Advanced") #endif - .listStyle(PlainListStyle()) .task { if let portraitUrl = URL(string: "https://picsum.photos/id/\(Int.random(in: 1...100))/400/600") { - self.messages.append(.init(user: MockMessages.chatbot, messageKind: .image(.remote(portraitUrl)))) + self.messages.append(.init(user: MessageMocker.chatbot, messageKind: .image(.remote(portraitUrl)))) } self.messages.append( .init( - user: MockMessages.chatbot, + user: MessageMocker.chatbot, messageKind: .text("https://github.com/EnesKaraosman/SwiftyChat and here is his phone +90 537 844 11-41, & mail: eneskaraosman53@gmail.com Today is 27 May 2020") ) ) - self.messages.append(contentsOf: MockMessages.generatedMessages(count: 53)) + self.messages.append(contentsOf: MessageMocker.generate(count: 53)) } } } diff --git a/Example/Example/BasicExampleView.swift b/Example/Example/BasicExampleView.swift index a9e21054..cb275cfc 100644 --- a/Example/Example/BasicExampleView.swift +++ b/Example/Example/BasicExampleView.swift @@ -7,10 +7,11 @@ import SwiftUI import SwiftyChat +import SwiftyChatMock struct BasicExampleView: View { - @State var messages: [MockMessages.ChatMessageItem] = MockMessages.generateMessage(kind: .Text, count: 20) + @State var messages: [MessageMocker.ChatMessageItem] = MessageMocker.generate(kind: .text, count: 20) @State private var message = "" @@ -19,14 +20,14 @@ struct BasicExampleView: View { } private var chatView: some View { - ChatView(messages: $messages) { + ChatView(messages: $messages) { BasicInputView( message: $message, placeholder: "Type something", onCommit: { messageKind in self.messages.append( - .init(user: MockMessages.sender, messageKind: messageKind, isSender: true) + .init(user: MessageMocker.sender, messageKind: messageKind, isSender: true) ) } ) diff --git a/Package.swift b/Package.swift index b3f266c7..e87bfbc9 100644 --- a/Package.swift +++ b/Package.swift @@ -12,17 +12,19 @@ let package = Package( products: [ .library( name: "SwiftyChat", - targets: ["SwiftyChat"]), + targets: ["SwiftyChat"] + ), + .library( + name: "SwiftyChatMock", + targets: ["SwiftyChatMock"] + ), ], dependencies: [ - // Image downloading library .package(url: "https://github.com/onevcat/Kingfisher.git", from: "7.11.0"), .package(url: "https://github.com/EnesKaraosman/SwiftUIEKtensions.git", from: "0.4.0"), .package(url: "https://github.com/dkk/WrappingHStack.git", from: "2.2.11") ], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "SwiftyChat", dependencies: [ @@ -32,6 +34,12 @@ let package = Package( ], exclude: ["Demo/Preview"] + ), + .target( + name: "SwiftyChatMock", + dependencies: [ + "SwiftyChat" + ] ) ], swiftLanguageVersions: [.v5] diff --git a/README.md b/README.md index 8beb197b..f5bf0650 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,9 @@ Here below is minimum code required to get started (see up & running)
For detail, visit example project [here](../master/SwiftyChatExample/Example) ```swift +import SwiftyChat +import SwiftyChatMock + @State private var scrollToBottom = false @State private var messages: [MockMessages.ChatMessageItem] = [] // for quick test assign MockMessages.generatedMessages() diff --git a/Sources/SwiftyChat/MessageViews/LoadingMessageView.swift b/Sources/SwiftyChat/MessageViews/LoadingMessageView.swift index fed58a08..1212bf17 100644 --- a/Sources/SwiftyChat/MessageViews/LoadingMessageView.swift +++ b/Sources/SwiftyChat/MessageViews/LoadingMessageView.swift @@ -45,12 +45,3 @@ struct LoadingMessageView: View { ) } } - -#if DEBUG -struct Loadingcell_Previews: PreviewProvider { - static var previews: some View { - LoadingMessageView(message: MockMessages.generateMessage(kind: .Text), size: .zero) - .environmentObject(ChatMessageCellStyle.init()) - } -} -#endif diff --git a/Sources/SwiftyChat/Mock/Lorem.swift b/Sources/SwiftyChatMock/Lorem.swift similarity index 92% rename from Sources/SwiftyChat/Mock/Lorem.swift rename to Sources/SwiftyChatMock/Lorem.swift index b6509284..3bd765f5 100755 --- a/Sources/SwiftyChat/Mock/Lorem.swift +++ b/Sources/SwiftyChatMock/Lorem.swift @@ -26,13 +26,13 @@ import Foundation // MARK: - Lorem -public class Lorem { +class Lorem { // MARK: Public /// Return a random word. /// /// - returns: Returns a random word. - public class func word() -> String { + class func word() -> String { wordList.randomElement()! } @@ -41,7 +41,7 @@ public class Lorem { /// - parameter count: The number of words to return. /// /// - returns: Returns an array of `count` words. - public class func words(nbWords: Int = 3) -> [String] { + class func words(nbWords: Int = 3) -> [String] { (1...nbWords).map { _ in word() } @@ -52,7 +52,7 @@ public class Lorem { /// - parameter count: The number of words the string should contain. /// /// - returns: Returns a string of `count` words. - public class func words(nbWords: Int = 3) -> String { + class func words(nbWords: Int = 3) -> String { words(nbWords: nbWords).joined(separator: " ") } @@ -61,7 +61,7 @@ public class Lorem { /// - parameter variable: If `true`, the number of words will vary between /// +/- 40% of `nbWords`. /// - returns: - public class func sentence(nbWords: Int = 6, variable: Bool = true) -> String { + class func sentence(nbWords: Int = 6, variable: Bool = true) -> String { if nbWords <= 0 { return "" } @@ -75,7 +75,7 @@ public class Lorem { /// - parameter nbSentences: The number of sentences to generate. /// /// - returns: Returns an array of random sentences. - public class func sentences(nbSentences: Int = 3) -> [String] { + class func sentences(nbSentences: Int = 3) -> [String] { (0 ..< nbSentences).map { _ in sentence() } } @@ -85,7 +85,7 @@ public class Lorem { /// - parameter variable: If `true`, the number of sentences will vary /// between +/- 40% of `nbSentences`. /// - returns: Returns a paragraph with `nbSentences` random sentences. - public class func paragraph(nbSentences: Int = 3, variable: Bool = true) -> String { + class func paragraph(nbSentences: Int = 3, variable: Bool = true) -> String { if nbSentences <= 0 { return "" } @@ -97,14 +97,14 @@ public class Lorem { /// Generate an array of random paragraphs. /// - parameter nbParagraphs: The number of paragraphs to generate. /// - returns: Returns an array of `nbParagraphs` paragraphs. - public class func paragraphs(nbParagraphs: Int = 3) -> [String] { + class func paragraphs(nbParagraphs: Int = 3) -> [String] { (0 ..< nbParagraphs).map { _ in paragraph() } } /// Generate a string of random paragraphs. /// - parameter nbParagraphs: The number of paragraphs to generate. /// - returns: Returns a string of random paragraphs. - public class func paragraphs(nbParagraphs: Int = 3) -> String { + class func paragraphs(nbParagraphs: Int = 3) -> String { paragraphs(nbParagraphs: nbParagraphs).joined(separator: "\n\n") } @@ -112,7 +112,7 @@ public class Lorem { /// - parameter maxNbChars: The maximum number of characters the string /// should contain. /// - returns: Returns a string of at most `maxNbChars` characters. - public class func text(maxNbChars: Int = 200) -> String { + class func text(maxNbChars: Int = 200) -> String { var result: [String] = [] if maxNbChars < 5 { @@ -201,7 +201,7 @@ public class Lorem { ] } -extension String { +private extension String { var firstCapitalized: String { var string = self string.replaceSubrange( @@ -213,8 +213,8 @@ extension String { } } -extension Int { - public func randomize(variation: Int) -> Int { +private extension Int { + func randomize(variation: Int) -> Int { let randomInt = Int.random(in: (100 - variation)..<(100+variation)) let multiplier = Double(randomInt) / 100 let randomized = Double(self) * multiplier diff --git a/Sources/SwiftyChat/Mock/MockMessages.swift b/Sources/SwiftyChatMock/MessageMocker.swift similarity index 73% rename from Sources/SwiftyChat/Mock/MockMessages.swift rename to Sources/SwiftyChatMock/MessageMocker.swift index d58bf6b3..32093d28 100644 --- a/Sources/SwiftyChat/Mock/MockMessages.swift +++ b/Sources/SwiftyChatMock/MessageMocker.swift @@ -8,121 +8,20 @@ // swiftlint:disable identifier_name import Foundation +import SwiftyChat -public struct MockMessages { +public struct MessageMocker { public enum Kind { - case Text - case Image - case Location - case Contact - case QuickReply - case Carousel - case Video - case Custom - - private var messageKind: ChatMessageKind { - switch self { - case .Text: return .text("") - case .Image: return .image(.remote(URL(string: "")!)) - case .Location: return .location(LocationRow(latitude: .nan, longitude: .nan)) - case .Contact: return .contact(ContactRow(displayName: "")) - case .QuickReply: return .quickReply([]) - case .Carousel: return .carousel([CarouselRow(title: "", imageURL: nil, subtitle: "", buttons: [])]) - case .Video: return .video( - VideoRow( - url: URL(string: "")!, - placeholderImage: .remote(URL(string: "")!), - pictureInPicturePlayingMessage: "" - ) - ) - case .Custom: return .custom("") - } - } - } - - // MARK: - Concrete model for Location - private struct LocationRow: LocationItem { - var latitude: Double - var longitude: Double - } - - // MARK: - Concrete model for Contact - private struct ContactRow: ContactItem { - var displayName: String - var image: PlatformImage? - var initials: String = "" - var phoneNumbers: [String] = [] - var emails: [String] = [] - } - - // MARK: - Concrete model for QuickReply - private struct QuickReplyRow: QuickReplyItem { - var title: String - var payload: String - } - - // MARK: - Concrete model for Carousel - private struct CarouselRow: CarouselItem { - var title: String - var imageURL: URL? - var subtitle: String - var buttons: [CarouselItemButton] - } - - // MARK: - Concrete model for Video - private struct VideoRow: VideoItem { - var url: URL - var placeholderImage: ImageLoadingKind - var pictureInPicturePlayingMessage: String - } - - // MARK: - Concrete model for ChatMessage - public struct ChatMessageItem: ChatMessage { - - public let id = UUID() - public var user: ChatUserItem - public var messageKind: ChatMessageKind - public var isSender: Bool - public var date: Date - - public init( - user: ChatUserItem, - messageKind: ChatMessageKind, - isSender: Bool = false, - date: Date = .init() - ) { - self.user = user - self.messageKind = messageKind - self.isSender = isSender - self.date = date - } - } - - // MARK: - Concrete model for ChatUser - public struct ChatUserItem: ChatUser { - - public static func == (lhs: ChatUserItem, rhs: ChatUserItem) -> Bool { - lhs.id == rhs.id - } - - public let id = UUID().uuidString - - /// Username - public var userName: String - - /// User's chat profile image, considered if `avatarURL` is nil - public var avatar: PlatformImage? - - /// User's chat profile image URL - public var avatarURL: URL? - - public init(userName: String, avatarURL: URL? = nil, avatar: PlatformImage? = nil) { - self.userName = userName - self.avatar = avatar - self.avatarURL = avatarURL - } - + case text + case image + case imageText + case location + case contact + case quickReply + case carousel + case video + case custom } public static var sender: ChatUserItem = .init( @@ -139,18 +38,16 @@ public struct MockMessages { [sender, chatbot].randomElement()! } - public static var mockImages: [PlatformImage] = [] - - public static func generateMessage(kind: MockMessages.Kind, count: UInt) -> [ChatMessageItem] { - (1...count).map { _ in generateMessage(kind: kind) } + public static func generate(kind: MessageMocker.Kind, count: UInt) -> [ChatMessageItem] { + (1...count).map { _ in generate(kind: kind) } } - public static func generateMessage(kind: MockMessages.Kind) -> ChatMessageItem { + public static func generate(kind: MessageMocker.Kind) -> ChatMessageItem { let randomUser = Self.randomUser switch kind { - case .Image: + case .image: let randomId = Int.random(in: 1...100) guard let url = URL(string: "https://picsum.photos/id/\(randomId)/800/600") else { fallthrough @@ -162,14 +59,26 @@ public struct MockMessages { isSender: randomUser == Self.sender ) - case .Text: + case .text: return ChatMessageItem( user: randomUser, messageKind: .text(Lorem.sentence()), isSender: randomUser == Self.sender ) - case .Carousel: + case .imageText: + let randomId = Int.random(in: 1...100) + guard let url = URL(string: "https://picsum.photos/id/\(randomId)/800/600") else { + fallthrough + } + + return ChatMessageItem( + user: randomUser, + messageKind: .imageText(.remote(url), Lorem.sentence()), + isSender: randomUser == Self.sender + ) + + case .carousel: return ChatMessageItem( user: Self.chatbot, messageKind: .carousel([ @@ -193,7 +102,7 @@ public struct MockMessages { isSender: false ) - case .QuickReply: + case .quickReply: let quickReplies: [QuickReplyRow] = (1...Int.random(in: 2...4)).map { idx in return QuickReplyRow(title: "Option.\(idx)", payload: "opt\(idx)") } @@ -203,7 +112,7 @@ public struct MockMessages { isSender: randomUser == Self.sender ) - case .Location: + case .location: let location = LocationRow( latitude: Double.random(in: 36...42), longitude: Double.random(in: 26...45) @@ -214,7 +123,7 @@ public struct MockMessages { isSender: randomUser == Self.sender ) - case .Contact: + case .contact: let contacts = [ ContactRow(displayName: "Enes Karaosman"), ContactRow(displayName: "John Doe"), @@ -226,7 +135,7 @@ public struct MockMessages { isSender: randomUser == Self.sender ) - case .Video: + case .video: let videoItem = VideoRow( url: URL(string: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")!, placeholderImage: .remote(URL(string: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg")!), @@ -238,7 +147,7 @@ public struct MockMessages { messageKind: .video(videoItem), isSender: randomUser == Self.sender ) - case .Custom: + case .custom: return ChatMessageItem( user: randomUser, messageKind: .custom("Custom Message Kind 😍🥰"), @@ -247,26 +156,104 @@ public struct MockMessages { } } - public static var randomMessageKind: MockMessages.Kind { + public static var randomMessageKind: MessageMocker.Kind { return [ - .Image, - .Text, .Text, - .Image, - .Text, .Text, - .Contact, - .Text, .Text, - .Carousel, - .Text, .Text, - .Location, - .Text, .Text, - .Video, - .Text, .Text, - .QuickReply, - .Custom + .image, + .text, .text, + .image, + .text, .text, + .imageText, + .contact, + .text, .text, + .carousel, + .text, .text, + .location, + .text, .text, + .video, + .text, .text, + .quickReply, + .custom ].randomElement()! } - public static func generatedMessages(count: Int = 30) -> [ChatMessageItem] { - (1...count).map { _ in generateMessage(kind: randomMessageKind)} + public static func generate(count: Int = 30) -> [ChatMessageItem] { + (1...count).map { _ in generate(kind: randomMessageKind)} + } +} + +// MARK: - Concrete implementation for message protocols +extension MessageMocker { + private struct LocationRow: LocationItem { + var latitude: Double + var longitude: Double + } + + private struct ContactRow: ContactItem { + var displayName: String + var image: PlatformImage? + var initials: String = "" + var phoneNumbers: [String] = [] + var emails: [String] = [] + } + + private struct QuickReplyRow: QuickReplyItem { + var title: String + var payload: String + } + + private struct CarouselRow: CarouselItem { + var title: String + var imageURL: URL? + var subtitle: String + var buttons: [CarouselItemButton] + } + + private struct VideoRow: VideoItem { + var url: URL + var placeholderImage: ImageLoadingKind + var pictureInPicturePlayingMessage: String + } + + public struct ChatMessageItem: ChatMessage { + public let id = UUID() + public var user: ChatUserItem + public var messageKind: ChatMessageKind + public var isSender: Bool + public var date: Date + + public init( + user: ChatUserItem, + messageKind: ChatMessageKind, + isSender: Bool = false, + date: Date = .init() + ) { + self.user = user + self.messageKind = messageKind + self.isSender = isSender + self.date = date + } + } + + public struct ChatUserItem: ChatUser { + public static func == (lhs: ChatUserItem, rhs: ChatUserItem) -> Bool { + lhs.id == rhs.id + } + + public let id = UUID().uuidString + + /// Username + public var userName: String + + /// User's chat profile image, considered if `avatarURL` is nil + public var avatar: PlatformImage? + + /// User's chat profile image URL + public var avatarURL: URL? + + public init(userName: String, avatarURL: URL? = nil, avatar: PlatformImage? = nil) { + self.userName = userName + self.avatar = avatar + self.avatarURL = avatarURL + } } }