Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Proof of concept for PreviewProvider #24

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Sources/PubNubChatComponents/ManagedEntityViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,13 @@ extension PubNubManagedChannel: ManagedChannelViewModel {
.eraseToAnyPublisher()
}

public var messagesPublisher: AnyPublisher<Set<MessageViewModel>, Never> {
// This line shouldn't be changed.
// I got the compiler error while adopting `ManagedMessageViewModel` for my custom struct defined in SharedPreviewData.swift
// That's why I haven't solved it yet and created a temporary workaround. The error message says:
//
// "Reference to invalid associated type MessageViewModel of type PubNubManagedChannel"
//
public var messagesPublisher: AnyPublisher<Set<PubNubManagedMessage>, Never> {
return publisher(for: \.messages).eraseToAnyPublisher()
}

Expand Down
92 changes: 92 additions & 0 deletions Sources/PubNubChatComponents/Previews/Previews.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//
// File 2.swift
//
//
// Created by Jakub Guz on 9/14/22.
//

import PubNub
import PubNubChat
import CoreData
import Combine
import UIKit
import SwiftUI
import ChatLayout

#if canImport(SwiftUI) && DEBUG
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we still need DEBUG for PreviewProvider? It should be removed by compiler

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this at all, what situation would SwiftUI not be importable? Linux?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, we don't need it. You can easily import SwiftUI for all Apple's platforms so this check is redundant. Another idea could be storing all previews in a separate target like you recently suggested so perhaps we could avoid adding extra macros.


struct ExamplePreview: PreviewProvider {

static var previews: some View {

Group() {

// Light Mode
UIViewPreview() {
let view = MessageListItemCell()
view.configure(MyViewModel(), theme: .incomingGroupChat)
return view
}
.previewLayout(.fixed(width: 414, height: 140))
.previewDisplayName("Light Mode")
.preferredColorScheme(.light)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These three modifiers could be packaged together as a single modifier to simplify reuse when we add similar configurations to other previews. (Same for the following examples)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still a WIP or should this be reviewed for merging?

It's not even a work in progress, but a quick POC instead which I wanted to share with you to make a decision. I can provide a more robust PR converted to Ready for review if our decision is to have previews for our UIKit views.


// Dark Mode
UIViewPreview() {
let view = MessageListItemCell()
view.configure(MyViewModel(), theme: .incomingGroupChat)
return view
}
.previewLayout(.fixed(width: 414, height: 140))
.previewDisplayName("Dark Mode")
.preferredColorScheme(.dark)

// RTL
UIViewPreview() {
let view = MessageListItemCell()
view.configure(MyViewModel(), theme: .incomingGroupChat)
return view
}
.environment(\.layoutDirection, .rightToLeft)
.previewLayout(.fixed(width: 414, height: 140))
.previewDisplayName("RTL")

// Accessibility
UIViewPreview() {

let view = MessageListItemCell()
view.configure(MyViewModel(), theme: .incomingGroupChat)
return view
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UIViewPreview contains always the same view, it can be created once in function

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally in that way it will be more self-describing and comments would not be necessary

Copy link
Contributor Author

@jguz-pubnub jguz-pubnub Sep 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm sure there are better options to solve it. I just wanted to quickly check if it's ever possible to use previews in Chat Components (it's a spike) and what are possible limitations so don't treat the current implementation very serious :)

}
.environment(\.sizeCategory, .accessibilityExtraLarge)
.previewLayout(.fixed(width: 414, height: 140))
.previewDisplayName("Accessibility")

UIViewControllerPreview() {
let vm = testChatProvider.senderMembershipsChanneListComponentViewModel()
let vc = vm.configuredComponentView()
let navv = UINavigationController(rootViewController: vc)

return navv
}
.previewDisplayName("VC")

if #available(iOS 15.0, *) {
UIViewControllerPreview() {
let vm = testChatProvider.senderMembershipsChanneListComponentViewModel()
let vc = vm.configuredComponentView()
let navv = UINavigationController(rootViewController: vc)

return navv

}
.previewInterfaceOrientation(.landscapeRight)
.previewDisplayName("VC2")
} else {
// Fallback on earlier versions
}
}
}
}

#endif
186 changes: 186 additions & 0 deletions Sources/PubNubChatComponents/Previews/SharedPreviewData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
//
// File 2.swift
//
//
// Created by Jakub Guz on 9/19/22.
//

#if canImport(SwiftUI) && DEBUG

import Foundation
import PubNub
import PubNubChat
import CoreData
import Combine

// The idea could be creating a shared class (or something similar) that holds shared data.
// Then it's up to you what data you would like to use in your previews. Please don't treat existing implementation very serious, it's rather a POC

// MARK: ChatProvider with in-memory storage

public let testChatProvider: PubNubChatProvider = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if I am wrong, but current design supports inheritance so you can make new TestChatProvider class. Is it safe to share testChatProvider between all of previews?

Copy link
Contributor Author

@jguz-pubnub jguz-pubnub Sep 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be safe as long as provider remains immutable. It may not be possible though so we could separately create testChatProvider for each preview.


let chatProvider = PubNubChatProvider(
pubnubProvider: PubNub(configuration: PubNubConfiguration(publishKey: "...", subscribeKey: "...", userId: "JG")),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that it is not working but is SDK sending request to API with this keys?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you interact with your view controller in the Live Preview mode then it's not possible to avoid these requests. But on the other hand, I don't think that PreviewProvider is a good place to test message sending or other features that require real communication. I think in this case it's better to involve getting-started app and run it with your own keys on iOS simulator. It's much more convenient than doing it in the preview window.

In my opinion, previews should be isolated and give you quick answer whether your view (or view controller) looks as expected for various scenarios so the fastest possible way to achieve it should be providing them mock data. Anyway, we could also consider if it's possible to disable any traffic between Components and PubNub server for such previews.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats true, preview should not send any requests and I am thinking if it is easy to mock provider, it would help here and in unit tests.

coreDataProvider: try! CoreDataProvider(location: .memory, flushDataOnLoad: false),
cacheProvider: UserDefaults.standard
)

let channel = PubNubChatChannel(
id: "CHANNEL1",
name: "Channel 1",
type: "direct",
status: "status",
details: "Channel details",
avatarURL: URL(string: "https://picsum/photos/200/300"),
updated: nil,
eTag: nil,
custom: VoidCustomData()
)

let user1 = PubNubChatUser(
id: "JG",
name: "JG"
)

let message = PubNubChatMessage(
id: "12345",
text: "Hello, world!!!",
pubnubUserId: "JG",
pubnubChannelId: "CHANNEL1"
)

let membership = PubNubChatMember(
channelId: "CHANNEL1",
userId: "JG"
)

chatProvider.dataProvider.load(users: [user1])
chatProvider.dataProvider.load(members: [membership])
chatProvider.dataProvider.load(channels: [channel])
chatProvider.dataProvider.load(messages: [message], processMessageActions: false)

return chatProvider

}()

// MARK: ManagedMessageViewModel conformance

class MyViewModel: ManagedMessageViewModel {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there an issue with passing in our PubNubManaged entities directly instead of creating custom View Model implementations with dummy data?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was a quick try for this POC. I just checked and there are no issues with passing in our PubNubManaged entities.


public typealias Entity = PubNubManagedMessage
public typealias ChannelViewModel = PubNubManagedChannel
public typealias UserViewModel = PubNubManagedUser
public typealias MessageActionModel = PubNubManagedMessageAction

public var pubnubId: Timetoken { return 0 }
public var managedObjectId: NSManagedObjectID { return NSManagedObjectID() }

public func decodedContent<T: Decodable>(from: T.Type) throws -> T {
return try JSONDecoder().decode(T.self, from: Data())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need decision if we using return in single lines or not

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have been historically including the return, but we can default to the standard when we add SwiftLint and/or SwiftFormat plugin support.

}

public var messageContentTypePublisher: AnyPublisher<String, Never> {
Just("TEXT").eraseToAnyPublisher()
}

public var messageContentPublisher: AnyPublisher<Data, Never> {
Just(Data()).eraseToAnyPublisher()
}

public var messageTextPublisher: AnyPublisher<String, Never> {
Just(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore."
).eraseToAnyPublisher()
}

public var messageCustomPublisher: AnyPublisher<Data, Never> {
Just(Data()).eraseToAnyPublisher()
}

public var messageDateCreatedPublisher: AnyPublisher<Date, Never> {
Just(Date()).eraseToAnyPublisher()
}

public var messageActionsPublisher: AnyPublisher<Set<PubNubManagedMessageAction>, Never> {
Just(Set<PubNubManagedMessageAction>()).eraseToAnyPublisher()
}

public var messageActions: Set<PubNubManagedMessageAction> {
Set<PubNubManagedMessageAction>()
}

public var userViewModel: PubNubManagedUser {

let user = PubNubManagedUser(context: testChatProvider.coreDataContainer.viewContext)
user.avatarURL = URL(string: "https://picsum.photos/100/300")
user.id = "JG"
user.name = "Jakub Jakub Jakub"

return user
}

public var text: String {
String("Text Text Text !!!")
}

public var channelViewModel: PubNubManagedChannel {
PubNubManagedChannel()
}

public var messageActionViewModels: Set<PubNubManagedMessageAction> {
Set<PubNubManagedMessageAction>()
}
}

// MARK: ManagedChannelViewModel conformance

class MyChannelViewModel: ManagedChannelViewModel {

typealias Entity = PubNubManagedChannel
typealias MessageViewModel = PubNubManagedMessage

public var pubnubId: String { return String() }
public var managedObjectId: NSManagedObjectID { return NSManagedObjectID() }

public var channelNamePublisher: AnyPublisher<String?, Never> {
Just("Channel name").eraseToAnyPublisher()
}

public var channelDetailsPublisher: AnyPublisher<String?, Never> {
Just("Channel details").eraseToAnyPublisher()
}

public var channelAvatarUrlPublisher: AnyPublisher<URL?, Never> {
Just(URL(string: "https://picsum.photos/300/400")!).eraseToAnyPublisher()
}

public var channelTypePublisher: AnyPublisher<String, Never> {
Just("Default").eraseToAnyPublisher()
}
public var channelCustomPublisher: AnyPublisher<Data, Never> {
Just(Data()).eraseToAnyPublisher()
}

public var membershipPublisher: AnyPublisher<Set<PubNubManagedMember>, Never> {
Just(Set<PubNubManagedMember>()).eraseToAnyPublisher()
}

public var memberCountPublisher: AnyPublisher<Int, Never> {
Just(5).eraseToAnyPublisher()
}

public var presentMemberCountPublisher: AnyPublisher<Int, Never> {
Just(5).eraseToAnyPublisher()
}

public var messagesPublisher: AnyPublisher<Set<PubNubManagedMessage>, Never> {
Just(Set<PubNubManagedMessage>()).eraseToAnyPublisher()
}

public var oldestMessagePublisher: AnyPublisher<PubNubManagedMessage?, Never> {
Just(nil).eraseToAnyPublisher()
}
}

#endif
45 changes: 45 additions & 0 deletions Sources/PubNubChatComponents/Previews/UIKitPreview.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// File.swift
//
//
// Created by Jakub Guz on 9/14/22.
//

#if canImport(SwiftUI) && DEBUG

import SwiftUI

struct UIViewPreview<View: UIView>: UIViewRepresentable {
let view: View

init(_ builder: @escaping () -> View) {
view = builder()
}

func makeUIView(context: Context) -> UIView {
return view
}

func updateUIView(_ view: UIView, context: Context) {
view.setContentHuggingPriority(.defaultHigh, for: .vertical)
view.setContentHuggingPriority(.defaultHigh, for: .horizontal)
}
}

struct UIViewControllerPreview<ViewController: UIViewController>: UIViewControllerRepresentable {
let viewController: ViewController

init(_ builder: @escaping () -> ViewController) {
viewController = builder()
}

func makeUIViewController(context: Context) -> ViewController {
viewController
}

func updateUIViewController(_ uiViewController: ViewController, context: Context) {

}
}

#endif
Loading