Merge pull request #113 from omnivore-app/feature/tab-bar-alts

Remove TabBar on iPhone
This commit is contained in:
Satindar Dhillon
2022-02-23 13:40:13 -08:00
committed by GitHub
17 changed files with 86 additions and 333 deletions

View File

@ -29,21 +29,6 @@ public final class RootViewModel: ObservableObject {
registerFonts()
}
func updateWaitlistStatus() {
services.dataService.viewerPublisher().sink(
receiveCompletion: { completion in
guard case let .failure(error) = completion else { return }
print(error)
},
receiveValue: { [weak self] viewer in
guard let self = self else { return }
let isWaitlisted = viewer.isWaitlisted
self.services.authenticator.updateWaitlistStatus(isWaitlistedUser: isWaitlisted)
}
)
.store(in: &subscriptions)
}
func configurePDFProvider(pdfViewerProvider: @escaping (URL, PDFViewerViewModel) -> AnyView) {
PDFProvider.pdfViewerProvider = { [weak self] url, feedItem in
guard let self = self else { return AnyView(Text("")) }
@ -133,8 +118,6 @@ public struct RootView: View {
@ObservedObject private var viewModel: RootViewModel
@ObservedObject private var authenticator: Authenticator
private var primaryViewModel: PrimaryContentViewModel
public init(
pdfViewerProvider: ((URL, PDFViewerViewModel) -> AnyView)?,
intercomProvider: IntercomProvider?
@ -142,7 +125,6 @@ public struct RootView: View {
let rootViewModel = RootViewModel()
self.viewModel = rootViewModel
self.authenticator = rootViewModel.services.authenticator
self.primaryViewModel = PrimaryContentViewModel.make(services: rootViewModel.services)
#if DEBUG
if CommandLine.arguments.contains("--uitesting") {
@ -163,34 +145,30 @@ public struct RootView: View {
@ViewBuilder private var innerBody: some View {
if authenticator.isLoggedIn {
if authenticator.isWaitlisted {
WaitlistView(viewModel: WaitlistViewModel.make(services: viewModel.services))
} else {
PrimaryContentView(viewModel: primaryViewModel)
.onAppear {
viewModel.updateWaitlistStatus()
viewModel.triggerPushNotificationRequestIfNeeded()
PrimaryContentView(services: viewModel.services)
.onAppear {
viewModel.triggerPushNotificationRequestIfNeeded()
}
#if os(iOS)
.fullScreenCover(item: $viewModel.webLinkPath, content: { safariLinkPath in
NavigationView {
FullScreenWebAppView(
viewModel: viewModel.webAppWrapperViewModel(webLinkPath: safariLinkPath.path),
handleClose: { viewModel.webLinkPath = nil }
)
}
#if os(iOS)
.fullScreenCover(item: $viewModel.webLinkPath, content: { safariLinkPath in
NavigationView {
FullScreenWebAppView(
viewModel: viewModel.webAppWrapperViewModel(webLinkPath: safariLinkPath.path),
handleClose: { viewModel.webLinkPath = nil }
)
}
})
#endif
.snackBar(
isShowing: $viewModel.showSnackbar,
text: Text(viewModel.snackbarMessage ?? "")
)
#if os(iOS)
.customAlert(isPresented: $viewModel.showPushNotificationPrimer) {
pushNotificationPrimerView
}
#endif
}
})
#endif
.snackBar(
isShowing: $viewModel.showSnackbar,
text: Text(viewModel.snackbarMessage ?? "")
)
#if os(iOS)
.customAlert(isPresented: $viewModel.showPushNotificationPrimer) {
pushNotificationPrimerView
}
#endif
} else {
WelcomeView(viewModel: WelcomeViewModel.make(services: viewModel.services))
.accessibilityElement()

View File

@ -11,6 +11,10 @@ extension HomeFeedViewModel {
LinkItemDetailViewModel.make(feedItem: feedItem, services: services)
}
if UIDevice.isIPhone {
viewModel.profileContainerViewModel = ProfileContainerViewModel.make(services: services)
}
viewModel.bind(services: services)
viewModel.loadItems(dataService: services.dataService, searchQuery: nil, isRefresh: false)
return viewModel

View File

@ -1,24 +0,0 @@
import Services
import SwiftUI
import Views
extension PrimaryContentViewModel {
static func make(services: Services) -> PrimaryContentViewModel {
let viewModel = PrimaryContentViewModel(
homeFeedViewModel: HomeFeedViewModel.make(services: services),
profileContainerViewModel: ProfileContainerViewModel.make(services: services)
)
viewModel.bind(services: services)
return viewModel
}
func bind(services _: Services) {
performActionSubject.sink { action in
switch action {
case .nothing:
break
}
}
.store(in: &subscriptions)
}
}

View File

@ -1,39 +0,0 @@
import Models
import Services
import SwiftUI
import Utils
import Views
extension WaitlistViewModel {
static func make(services: Services) -> WaitlistViewModel {
let viewModel = WaitlistViewModel()
viewModel.bind(services: services)
return viewModel
}
func bind(services: Services) {
performActionSubject.sink { [weak self] action in
switch action {
case .logout:
services.authenticator.logout()
case .checkStatus:
self?.updateWaitlistStatus(services: services)
}
}
.store(in: &subscriptions)
}
private func updateWaitlistStatus(services: Services) {
services.dataService.viewerPublisher().sink(
receiveCompletion: { completion in
guard case let .failure(error) = completion else { return }
print(error)
},
receiveValue: { viewer in
let isWaitlisted = viewer.isWaitlisted
services.authenticator.updateWaitlistStatus(isWaitlistedUser: isWaitlisted)
}
)
.store(in: &subscriptions)
}
}

View File

@ -1,34 +1,15 @@
import Combine
import Models
import Services
import SwiftUI
public final class PrimaryContentViewModel: ObservableObject {
let categories: [PrimaryContentCategory]
public enum Action {
case nothing
}
public var subscriptions = Set<AnyCancellable>()
public let performActionSubject = PassthroughSubject<Action, Never>()
public init(
homeFeedViewModel: HomeFeedViewModel,
profileContainerViewModel: ProfileContainerViewModel
) {
self.categories = [
.feed(viewModel: homeFeedViewModel),
.profile(viewModel: profileContainerViewModel)
]
}
}
import Views
public struct PrimaryContentView: View {
@ObservedObject private var viewModel: PrimaryContentViewModel
@State private var currentTab = 0
let homeFeedViewModel: HomeFeedViewModel
let profileContainerViewModel: ProfileContainerViewModel
public init(viewModel: PrimaryContentViewModel) {
self.viewModel = viewModel
public init(services: Services) {
self.homeFeedViewModel = HomeFeedViewModel.make(services: services)
self.profileContainerViewModel = ProfileContainerViewModel.make(services: services)
}
public var body: some View {
@ -45,27 +26,23 @@ public struct PrimaryContentView: View {
// iphone view container
private var compactView: some View {
TabView(selection: $currentTab) {
ForEach(viewModel.categories.indices, id: \.self) { index in
viewModel.categories[index].destinationView
.tabItem {
TabIcon(isSelected: currentTab == index, primaryContentCategory: viewModel.categories[index])
}
.tag(index)
}
}
.accentColor(.appGrayTextContrast)
HomeFeedView(viewModel: homeFeedViewModel)
}
// ipad and mac view container
private var regularView: some View {
NavigationView {
let categories = [
PrimaryContentCategory.feed(viewModel: homeFeedViewModel),
PrimaryContentCategory.profile(viewModel: profileContainerViewModel)
]
return NavigationView {
// The first column is the sidebar.
PrimaryContentSidebar(categories: viewModel.categories)
PrimaryContentSidebar(categories: categories)
.navigationTitle("Categories")
// Initial Content of second column
if let destinationView = viewModel.categories.first?.destinationView {
if let destinationView = categories.first?.destinationView {
destinationView
} else {
Text("Select a Category")

View File

@ -50,19 +50,16 @@ public struct Viewer {
public let username: String
public let name: String
public let profileImageURL: String?
public let isWaitlisted: Bool
public init(
username: String,
name: String,
profileImageURL: String?,
isWaitlisted: Bool,
userID: String
) {
self.username = username
self.name = name
self.profileImageURL = profileImageURL
self.isWaitlisted = isWaitlisted
self.userID = userID
}
}

View File

@ -14,7 +14,6 @@ public final class Authenticator: ObservableObject {
}
@Published public internal(set) var isLoggedIn: Bool
@Published public internal(set) var isWaitlisted = false
let networker: Networker
@ -58,10 +57,6 @@ public final class Authenticator: ObservableObject {
return !authToken.isEmpty
}
public func updateWaitlistStatus(isWaitlistedUser: Bool) {
isWaitlisted = isWaitlistedUser
}
private func clearCookies() {
HTTPCookieStorage.shared.removeCookies(since: Date.distantPast)

View File

@ -30,7 +30,6 @@ extension DataService {
profileImageURL: try $0.profile(
selection: .init { try $0.pictureUrl() }
),
isWaitlisted: try !($0.isFullUser() ?? false),
userID: try $0.id()
)
}

View File

@ -15,5 +15,6 @@ extension Image {
static var homeTab: Image { Image("_homeTab", bundle: .module) }
static var homeTabSelected: Image { Image("_homeTabSelected", bundle: .module) }
static var profileTab: Image { Image("_profileTab", bundle: .module) }
static var profile: Image { Image("_profile", bundle: .module) }
static var profileTabSelected: Image { Image("_profileTabSelected", bundle: .module) }
}

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "user-circle-gear.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" fill="#000000" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"></rect><circle cx="128" cy="120" r="40" fill="none" stroke="#000000" stroke-miterlimit="10" stroke-width="16"></circle><path d="M63.8,199.4a72,72,0,0,1,128.4,0" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></path><circle cx="200" cy="56" r="16" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></circle><line x1="200" y1="40" x2="200" y2="28" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></line><line x1="186.1" y1="48" x2="175.8" y2="42" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></line><line x1="186.1" y1="64" x2="175.8" y2="70" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></line><line x1="200" y1="72" x2="200" y2="84" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></line><line x1="213.9" y1="64" x2="224.2" y2="70" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></line><line x1="213.9" y1="48" x2="224.2" y2="42" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></line><path d="M223.3,116.5A87.7,87.7,0,0,1,224,128a96,96,0,1,1-96-96,87,87,0,0,1,8.9.4" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></path></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -6,6 +6,7 @@ import Utils
public final class HomeFeedViewModel: ObservableObject {
let detailViewModelCreator: (FeedItem) -> LinkItemDetailViewModel
var currentDetailViewModel: LinkItemDetailViewModel?
public var profileContainerViewModel: ProfileContainerViewModel?
@Published public var items = [FeedItem]()
@Published public var isLoading = false
@ -256,10 +257,26 @@ public struct HomeFeedView: View {
public var body: some View {
#if os(iOS)
if UIDevice.isIPhone {
if UIDevice.isIPhone, let profileContainerViewModel = viewModel.profileContainerViewModel {
NavigationView {
conditionalInnerBody
.toolbar {
ToolbarItem {
NavigationLink(
destination: {
ProfileContainerView(viewModel: profileContainerViewModel)
},
label: {
Image.profile
.resizable()
.frame(width: 26, height: 26)
.padding()
}
)
}
}
}
.accentColor(.appGrayTextContrast)
} else {
conditionalInnerBody
}

View File

@ -1,14 +1,14 @@
import SwiftUI
enum PrimaryContentCategory: Identifiable, Hashable, Equatable {
public enum PrimaryContentCategory: Identifiable, Hashable, Equatable {
case feed(viewModel: HomeFeedViewModel)
case profile(viewModel: ProfileContainerViewModel)
static func == (lhs: PrimaryContentCategory, rhs: PrimaryContentCategory) -> Bool {
public static func == (lhs: PrimaryContentCategory, rhs: PrimaryContentCategory) -> Bool {
lhs.id == rhs.id
}
var id: String {
public var id: String {
title
}
@ -39,11 +39,11 @@ enum PrimaryContentCategory: Identifiable, Hashable, Equatable {
}
}
var listLabel: some View {
public var listLabel: some View {
Label { Text(title) } icon: { image.renderingMode(.template) }
}
@ViewBuilder var destinationView: some View {
@ViewBuilder public var destinationView: some View {
switch self {
case let .feed(viewModel: viewModel):
HomeFeedView(viewModel: viewModel)
@ -52,24 +52,7 @@ enum PrimaryContentCategory: Identifiable, Hashable, Equatable {
}
}
func hash(into hasher: inout Hasher) {
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
struct TabIcon: View {
let isSelected: Bool
let primaryContentCategory: PrimaryContentCategory
var body: some View {
if isSelected {
Label {
Text(primaryContentCategory.title)
} icon: {
primaryContentCategory.selectedImage
}
} else {
primaryContentCategory.listLabel
}
}
}

View File

@ -30,16 +30,8 @@ public struct ProfileContainerView: View {
public var body: some View {
#if os(iOS)
if UIDevice.isIPhone {
NavigationView {
Form {
innerBody
}
}
} else {
Form {
innerBody
}
Form {
innerBody
}
#elseif os(macOS)
List {

View File

@ -1,125 +0,0 @@
import Combine
import Models
import SwiftUI
public final class WaitlistViewModel: ObservableObject {
public enum Action {
case logout
case checkStatus
}
public var subscriptions = Set<AnyCancellable>()
public let performActionSubject = PassthroughSubject<Action, Never>()
public init() {}
}
public struct WaitlistView: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@ObservedObject private var viewModel: WaitlistViewModel
public init(viewModel: WaitlistViewModel) {
self.viewModel = viewModel
}
@ViewBuilder func userInteractiveView(width: CGFloat) -> some View {
waitlistButtonView
.frame(width: width)
.zIndex(2)
}
var titleLogo: some View {
Image.omnivoreTitleLogo
.renderingMode(.template)
.foregroundColor(.appGrayTextContrast)
.frame(height: 40)
}
var waitlistButtonView: some View {
VStack(alignment: .center, spacing: 32) {
Text("Your username has been reserved. We will inform you by email when we open up Omnivore to more users.")
.font(.appHeadline)
.multilineTextAlignment(.center)
.frame(maxWidth: 300)
BorderedButton(color: .appGrayTextContrast, text: "Check Status") {
viewModel.performActionSubject.send(.checkStatus)
}
.frame(width: 220)
BorderedButton(color: .appGrayTextContrast, text: "Logout") {
viewModel.performActionSubject.send(.logout)
}
.frame(width: 220)
}
.padding(.horizontal, horizontalSizeClass == .compact ? 16 : 80)
.padding(.top, horizontalSizeClass == .compact ? 16 : 0)
}
@ViewBuilder func splitColorBackground(width: CGFloat) -> some View {
HStack(spacing: 0) {
Color.systemBackground.frame(width: width * 0.5)
Color.appBackground.frame(width: width * 0.5)
}
.edgesIgnoringSafeArea(.all)
}
@ViewBuilder func largeBackgroundImage(width: CGFloat) -> some View {
Image.readingIllustrationXXL
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: width)
.clipped()
.edgesIgnoringSafeArea([.vertical, .trailing])
}
@ViewBuilder func primaryContent() -> some View {
if horizontalSizeClass == .compact {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Color.systemBackground
.edgesIgnoringSafeArea(.all)
if geometry.size.width < geometry.size.height {
VStack {
Color.appDeepBackground.frame(height: 100)
Spacer()
}
.edgesIgnoringSafeArea(.all)
}
VStack {
if geometry.size.width < geometry.size.height {
RegistrationHeroImageView(
tapGestureHandler: {}
)
}
userInteractiveView(width: geometry.size.width)
Spacer()
}
}
}
} else {
GeometryReader { geometry in
ZStack(alignment: .leading) {
splitColorBackground(width: geometry.size.width)
VStack {
titleLogo
Spacer()
}
.padding()
HStack(spacing: 0) {
userInteractiveView(width: geometry.size.width * 0.5)
largeBackgroundImage(width: geometry.size.width * 0.5)
}
}
}
}
}
public var body: some View {
primaryContent()
}
}

View File

@ -42,9 +42,6 @@ struct MainApp: App {
WindowGroup {
RootView(pdfViewerProvider: nil, intercomProvider: nil)
}
// Settings {
// SettingsView()
// }
#endif
}
@ -52,18 +49,3 @@ struct MainApp: App {
AnyView(PDFViewer(pdfURL: url, viewModel: viewModel))
}
}
struct SettingsView: View {
var appVersion: String {
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
}
var body: some View {
VStack {
Text("Omnivore")
.font(.largeTitle)
Text("Omnivore Version: \(appVersion)")
}
.frame(width: 600, height: 600)
}
}