Skip to content

Commit

Permalink
release [1.4.2] - 2023-11-20
Browse files Browse the repository at this point in the history
#### Added

- Deep links support for nested NavigationStorages.
- Updated docs, tests and code comments.

#### Fixed

- Issue with async call in MainActor from deeplink methods `checkSubAction` and `replace`. This was reproduced only from iOS 16 (iOS 15, 17 don't).
  • Loading branch information
sofbix committed Nov 20, 2023
1 parent efb5084 commit f9c2938
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 62 deletions.
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@

All notable changes to this project will be documented in this file.

## [1.4.2] - 2023-11-20

#### Added

- Deep links support for nested NavigationStorages.
- Updated docs, tests and code comments.

#### Fixed

- Issue with async call in MainActor from deeplink methods `checkSubAction` and `replace`. This was reproduced only from iOS 16 (iOS 15, 17 don't).

## [1.4.1] - 2023-11-17

#### Added
Expand All @@ -11,7 +22,7 @@ All notable changes to this project will be documented in this file.

#### Fixed

- Supporting deeplink for other custom navigation.
- Deep links support for other custom navigation.

## [1.4.0] - 2023-11-13

Expand Down
4 changes: 3 additions & 1 deletion Example/NavigationExample/NavigationExample/BoolView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,9 @@ struct BoolView: View {
MainTabView()
}
.fullScreenCover(item: $firstModalData) { value in
FirstView(string: value.string)
NavigationViewStorage{
FirstView(string: value.string)
}
}
.navigateUrlParams("ModalFirstView") { params in
if let modalFirst = params.popStringParam("modalFirst") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ final class TabUITests: XCTestCase {
.tapBool()
BoolView(app: app)
.checkThis()
.enterActionUrlTextField("TabView/SecondView/BoolView/ModalFirstView?modalFirst=modal&secondNumber=1&tab=Second Tab")
.enterActionUrlTextField("TabView/BoolView/ModalFirstView?modalFirst=modal&tab=Second Tab")
.tapReplaceUrl()
FirstView(app: app)
.checkThis(string: "modal")
Expand All @@ -118,7 +118,7 @@ final class TabUITests: XCTestCase {
.tapBool()
BoolView(app: app)
.checkThis()
.enterActionUrlTextField("TabView/SecondView/BoolView?secondNumber=1&tab=Second Tab")
.enterActionUrlTextField("TabView/BoolView?tab=Second Tab")
.tapAppendUrl()
.checkThis()
.tapBack()
Expand Down Expand Up @@ -158,4 +158,65 @@ final class TabUITests: XCTestCase {
.checkThis(string: "TabBar")
}

func testAfterModalFirstFromUrl() throws {
let app = XCUIApplication.app(isTab: true)
MainView(app: app)
.checkThis()
.checkChanging(false)
.tapBool()
BoolView(app: app)
.checkThis()
.enterActionUrlTextField("TabView/ModalFirstView/SecondView/BoolView/FirstView?modalFirst=modal&secondNumber=2&tab=Bool Tab&firstString=navigation")
.tapReplaceUrl()
FirstView(app: app)
.checkThis(string: "navigation")
.tapDismiss()
BoolView(app: app)
.checkThis()
.tapDismiss()
SecondView(app: app)
.checkThis(number: 2)
.tapDismiss()
FirstView(app: app)
.checkThis(string: "modal")
.tapDismiss()
BoolView(app: app)
.checkThis()
}

func testChildrenNavigationFromUrl() throws {
let app = XCUIApplication.app(isTab: true)
MainView(app: app)
.checkThis()
.checkChanging(false)
.tapBool()
BoolView(app: app)
.checkThis()
.enterActionUrlTextField("TabView/SecondView/BoolView/ModalFirstView/SecondView/BoolView/ModalFirstView/SecondView?tab=First Tab&secondNumber=1&modalFirst=modal1&secondNumber=2&modalFirst=modal2&secondNumber=3")
.tapReplaceUrl()
SecondView(app: app)
.checkThis(number: 3, timeout: 7)
.swipeBack()
FirstView(app: app)
.checkThis(string: "modal2")
.tapDismiss()
BoolView(app: app)
.checkThis()
.tapDismiss()
SecondView(app: app)
.checkThis(number: 2)
.tapDismiss()
FirstView(app: app)
.checkThis(string: "modal1")
.tapDismiss()
BoolView(app: app)
.checkThis()
.swipeBack()
SecondView(app: app)
.checkThis(number: 1)
.swipeBack()
FirstView(app: app)
.checkThis(string: "TabBar")
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ struct FirstView: View {
var text = app.staticTexts["This is First"]
text.waitForExistingAndAssert(timeout: 5)
text = app.staticTexts["with: \(string)"]
text.waitForExistingAndAssert(timeout: 2)
text.waitForExistingAndAssert(timeout: 5)
return self
}

Expand Down Expand Up @@ -46,9 +46,15 @@ struct FirstView: View {

@discardableResult
func tapDismiss() -> Self {
let button = app.buttons["dismiss"].firstMatch
_ = button.waitForExistence(timeout: 2)
button.tap()
let buttons = app.buttons.matching(identifier: "dismiss")
for i in 0..<buttons.count{
let button = buttons.element(boundBy: i)
_ = button.waitForExistence(timeout: 2)
if button.isHittable {
button.tap()
return self
}
}
return self
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ struct SecondView: View {
let app: XCUIApplication

@discardableResult
func checkThis(number: Int) -> Self {
func checkThis(number: Int, timeout: TimeInterval = 5) -> Self {
var text = app.staticTexts["This is Second"]
text.waitForExistingAndAssert(timeout: 5)
text = app.staticTexts["with: \(number)"]
text.waitForExistingAndAssert(timeout: 5)
text.waitForExistingAndAssert(timeout: timeout)
return self
}

Expand Down
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ Now Developers have standard framework SwiftUI. Correct navigation features were

## Features

- [x] Full support SwiftUI, has declarative style
- [x] Supporting iOS 14, iOS 15, iOS 16, iOS 17
- [x] Target switching between NavigationView and NavigationStack
- [x] Fixing known Apple bugs
- [x] Has popTo, skip, isRoot and each other functions
- [x] Works with URL: simple supporting the deep links
- [x] UI tests coverage
- [x] Full support SwiftUI, has declarative style.
- [x] Supporting iOS 14, iOS 15, iOS 16, iOS 17.
- [x] Target switching between NavigationView and NavigationStack.
- [x] Fixing known Apple bugs.
- [x] Has popTo, skip, isRoot and each other functions.
- [x] Works with URL: simple supporting the deep links.
- [x] UI tests full coverage.

## Installation

Expand Down Expand Up @@ -204,6 +204,12 @@ struct MyTabView: View {

```

## Features of Nested Navigation

Since `NavigationStack` don't support nested `NavigationStack` it affected to `NavigationViewStorge` too. But it reproduced on iOS 16 and leter. On iOS 15.x and lower it work fine because `NavigationView` haven't this problem.

You can also seporate nested `NavigationViewStorge` with help another navigation for example .fullScreenCover or TabBar then you can use fearlessly nested `NavigationViewStorge` even with iOS 16 and leter. For this case, we even provided support for deep links of nested navigation.

## Common Functions

### NavigationStorage
Expand Down
23 changes: 15 additions & 8 deletions Sources/SUINavigation/Core/NavigationStorage+Url.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ extension NavigationStorage {
}

public func replace(with url: String) {
if let parentStorge = self.parentStorge {
parentStorge.replace(with: url)
return
}
popToRoot()
Task {
Task { @MainActor in
// wait 0.7 sec (animation of navigation)
try await Task.sleep(nanoseconds: 7_00_000_000)
await MainActor.run {
self.actionPath = NavigationActionPath(url: url)
}
actionPath = NavigationActionPath(url: url)
}
}

Expand All @@ -58,12 +60,10 @@ extension NavigationStorage {
}

func checkSubAction(id: String) {
Task {
Task { @MainActor in
// wait 0.7 sec (animation of navigation)
try await Task.sleep(nanoseconds: 7_00_000_000)
await MainActor.run {
removeSubAction(id: id)
}
removeSubAction(id: id)
}
}

Expand All @@ -76,6 +76,13 @@ extension NavigationStorage {
return
}

// If childStorge not nil We founed in anotner navigation storage. We should switch actionPath respond to
if let childStorage = childStorge {
self.actionPath = nil
childStorage.actionPath = actionPath
return
}

let children = pathItems.last?.children ?? rootChildren

if let action = children[actionName] {
Expand Down
46 changes: 9 additions & 37 deletions Sources/SUINavigation/Core/NavigationStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,25 @@ public final class NavigationStorage: ObservableObject {
}
}

// Stack of navigation without root
@Published
private(set) var pathItems: [Item] = []

// We don't have root from pathItems, so children of this item used by activate some navigation.
var rootChildren: [String: NavigateUrlParamsHandler] = [:]

// `childStorge` and `parentStorge` need for support nested NavigationStorage.
weak var childStorge: NavigationStorage? = nil
weak var parentStorge: NavigationStorage? = nil

// It activate navigation from path
var actionPath: NavigationActionPath? = nil {
didSet {
actionReactor()
}
}

// return uid if has duplicated
// To add View Info to Stack at navigation transition. It return id if has duplicates.
func addItem(isPresented: Binding<Bool>, id: String, param: NavigationParameter?) -> String? {
let hasTheSameId = pathItems.first(where: { $0.id == id }) != nil
let item = Item(isPresented: isPresented, id: id, param: param)
Expand All @@ -63,6 +70,7 @@ public final class NavigationStorage: ObservableObject {
}
}

// To remove View Info at navigation transition. Uid needs for double id
func removeItem(isPresented: Binding<Bool>, id: String, uid: String?) {
guard let foundIndex = pathItems.lastIndex(where: { $0.id == id && $0.uid == uid }) else {
return
Expand Down Expand Up @@ -147,39 +155,3 @@ public final class NavigationStorage: ObservableObject {
}
}

public struct NavigationViewStorage<Content: View>: View {
let content: Content

@StateObject
var navigationStorage = NavigationStorage()

public init(@ViewBuilder content: () -> Content) {
self.content = content()
}

public var body: some View {
navigation
.optionalEnvironmentObject(navigationStorage)
}

@ViewBuilder
private var navigation: some View {
if #available(iOS 16.0, *) {
// We can't use it from iOS 16 because
// The NavigationStack have an issue with dismiss many screens
// In the stack rest artefact empty screen by this case
// This issue fixed from iOS 17
NavigationStack {
content
}
} else {
NavigationView {
content
}
// bug from Apple: when change screen
// - dismiss to First View
// https://developer.apple.com/forums/thread/691242
.navigationViewStyle(.stack)
}
}
}
53 changes: 53 additions & 0 deletions Sources/SUINavigation/Core/NavigationViewStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// NavigationViewStorage.swift
//
//
// Created by Sergey Balalaev on 20.11.2023.
//

import SwiftUI

public struct NavigationViewStorage<Content: View>: View {
let content: Content

@OptionalEnvironmentObject
private var parentNavigationStorage: NavigationStorage?

@StateObject
var navigationStorage = NavigationStorage()

public init(@ViewBuilder content: () -> Content) {
self.content = content()
}

public var body: some View {
navigation
.onAppear{
if let parentNavigationStorage = parentNavigationStorage {
parentNavigationStorage.childStorge = navigationStorage
navigationStorage.parentStorge = parentNavigationStorage
}
}
.onDisappear(){
parentNavigationStorage?.childStorge = nil
}
.optionalEnvironmentObject(navigationStorage)
}

@ViewBuilder
private var navigation: some View {
if #available(iOS 16.0, *) {
NavigationStack {
content
}
} else {
NavigationView {
content
}
// bug from Apple: when change screen
// - dismiss to First View
// https://developer.apple.com/forums/thread/691242
.navigationViewStyle(.stack)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ struct NavigationItemModifier<Destination: View, Item: Equatable>: ViewModifier
func body(content: Content) -> some View {
ZStack {
if #available(iOS 16.0, *) {
// We can't use from iOS 17 .navigationDestination with item param because that has an issue with navigation
content
.navigationDestination(isPresented: $isActive, destination: {if let item = item.wrappedValue {destination(item)}})
} else {
Expand Down

0 comments on commit f9c2938

Please sign in to comment.