SwiftUI memory leak when using NavigationLink Push UIViewControllerRepresentable? - swiftui

This situation is no problem.enter link description here
So I'm confused, I don't know if this is a bug or whether Apple designed it this way.
struct HomePage: View {
var body: some View {
NavigationView {
VStack {
jumpView1
.padding()
.background(Color.red)
.padding()
jumpView2
.padding()
.background(Color.blue)
}
.navigationBarItems(leading:leftBarItem)
.navigationBarTitle("Home",displayMode: .inline)
}
}
var leftBarItem: some View {
NavigationLink(destination: CustomViewController()) {
Text("Go CustomViewController ")
}
}
var jumpView1: some View {
NavigationLink(destination: CustomViewController()) {
Text("Go CustomViewController")
.foregroundColor(.white)
}
}
var jumpView2: some View {
NavigationLink(destination: SecondPage()) {
Text("Go SecondPage")
.foregroundColor(.white)
}
}
}
struct FirstPage: View {
var body: some View {
NavigationLink(destination: CustomViewController()) {
Text("Go CustomViewController")
}
}
}
struct SecondPage: View {
var body: some View {
NavigationLink(destination: CustomViewController()) {
Text("Go CustomViewController")
}
}
}
the func dismantleUIViewController only call in leftBarItem Action,
and never call, when push in body NavigatinLink
struct CustomViewController: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> CustomRealViewController {
let vc = CustomRealViewController()
return vc
}
func updateUIViewController(_ uiViewController: CustomRealViewController, context: Context) {
}
static func dismantleUIViewController(_ uiViewController: CustomRealViewController, coordinator: Coordinator) {
print("dismiss")
}
}
the func deinit only call in leftBarItem Action,
and never call, when push in body NavigatinLink
class CustomRealViewController: UIViewController {
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
print("viewDidDisappear")
}
deinit {
print("deinit")
}
}

Related

SwiftUI Web View unable to go back and forward

I am using WebView for loading the html into view . I added the forward and back button to go back and forward with require code but the problem is when I enter the url and click more link , I do not see the back button or forward button is enable ..
Here is the content view ..
import SwiftUI
struct ContentView: View {
#State private var selection = 0
var body: some View {
TabView(selection: $selection) {
NavigationView {
WebListView().navigationBarTitle("Web View ", displayMode: .inline)
.toolbarBackground(Color.white,for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)
.accentColor(.red)
.onAppear() {
UITabBar.appearance().barTintColor = .white
}
}.tabItem {
Image(systemName: "person.crop.circle")
Text("Web View")
}.tag(2)
}
}
}
Here is the code ListView ..
import SwiftUI
struct WebListView: View {
#StateObject var model = WebViewModel()
var body: some View {
WebContentView()
.font(.system(size: 30, weight: .bold, design: .rounded))
.toolbar {
ToolbarItemGroup(placement: .automatic) {
Button(action: {
model.goBack()
}, label: {
Image(systemName: "chevron.left")
})
.disabled(!model.canGoBack)
.font(.system(size: 20))
Button(action: {
model.goForward()
}, label: {
Image(systemName: "chevron.right")
})
.disabled(!model.canGoForward)
.font(.system(size: 20))
Spacer()
}
}
}
}
Code for UIViewRepresentable..
struct WebView: UIViewRepresentable {
typealias UIViewType = WKWebView
let webView: WKWebView
func makeUIView(context: Context) -> WKWebView {
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) { }
}
Here is the WebContent view code ..
import Combine
import WebKit
import SwiftUI
#MainActor
struct WebContentView: View {
#StateObject var model = WebViewModel()
var body: some View {
ZStack(alignment: .bottom) {
Color.blue
VStack(spacing: 0) {
HStack(spacing: 10) {
HStack {
TextField("Enter url",
text: $model.urlString)
.keyboardType(.URL)
.autocapitalization(.none)
.disableAutocorrection(true)
.padding(8)
.font(.system(size: 15))
Spacer()
}
.background(Color.white)
.cornerRadius(30)
Button("GO", action: {
model.loadUrl()
})
.foregroundColor(.white)
.padding(10)
.font(.system(size: 15))
.background(.blue)
}.padding(10)
ZStack {
WebView(webView: model.webView)
if model.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
}
}
}
}
}
}
struct WebContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here is the view Model code ..
#MainActor
class WebViewModel: ObservableObject {
let webView: WKWebView
private let navigationDelegate: WebViewNavigationDelegate
init() {
let configuration = WKWebViewConfiguration()
configuration.websiteDataStore = .nonPersistent()
webView = WKWebView(frame: .zero, configuration: configuration)
navigationDelegate = WebViewNavigationDelegate()
webView.navigationDelegate = navigationDelegate
setupBindings()
}
#Published var urlString: String = ""
#Published var canGoBack: Bool = false
#Published var canGoForward: Bool = false
#Published var isLoading: Bool = false
private func setupBindings() {
webView.publisher(for: \.canGoBack)
.assign(to: &$canGoBack)
webView.publisher(for: \.canGoForward)
.assign(to: &$canGoForward)
webView.publisher(for: \.isLoading)
.assign(to: &$isLoading)
}
func loadUrl() {
guard let url = URL(string: urlString) else {
return
}
webView.load(URLRequest(url: url))
}
func goForward() {
webView.goForward()
}
func goBack() {
webView.goBack()
}
}
here is code for delegate ..
import WebKit
class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
// TODO
decisionHandler(.allow)
}
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
// TODO
decisionHandler(.allow)
}
}
Here is the screenshot ..
The problem is that you're creating a new WebViewModel in WebContentView
Change
struct WebContentView: View {
#StateObject var model = WebViewModel()
//etc
}
to
struct WebContentView: View {
#EnvironmentObject var model: WebViewModel
//etc
}
Then update hereā€¦
struct WebListView: View {
#StateObject var model = WebViewModel()
var body: some View {
WebContentView()
.environmentObject(model) // add environmentObject

Deleting a List entry in Swift UI

What is the correct way to delete an entry from a list? Where should the closure be placed?
#ObservedObject var category : Category
var body: some View {
List {
ForEach(category.reminders?.allObjects as! [Reminder]) { reminder in
NavigationLink(destination: ReminderDetail(reminder: reminder)) {
VStack {
Text(reminder.title!)
}
}
}
}
.navigationTitle("Reminders")
VStack {
NavigationLink(destination: AddReminder(category: category)) { Text("Add Reminder") }
}.padding()
}
You can try this:
var body: some View {
List {
ForEach(category.reminders?.allObjects as! [Reminder]) { reminder in
NavigationLink(destination: ReminderDetail(reminder: reminder)) {
VStack {
Text("reminder.title!")
}
}
}.onDelete(perform: self.deleteItem)
}
.navigationTitle("Reminders")
private func deleteItem(at indexSet: IndexSet) {
self.category.reminders(atOffsets: indexSet)
}

.confirmationDialog inside of .swipeActions does not work, iOS 15

With regards to iOS 15, Xcode 13; I am wondering if this is a bug, not properly implemented, or a planned non-functional feature...
With a list that has a .swipeActions that calls a .confirmationDialog the confirmation dialog does not show.
See example:
import SwiftUI
struct ContentView: View {
#State private var confirmDelete = false
var body: some View {
NavigationView {
List{
ForEach(1..<10) {_ in
Cell()
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
confirmDelete.toggle()
} label: {
Label("Delete", systemImage: "trash")
}
.confirmationDialog("Remove this?", isPresented: $confirmDelete) {
Button(role: .destructive) {
print("Removed!")
} label: {
Text("Yes, Remove this")
}
}
}
}
}
}
}
struct Cell: View {
var body: some View {
Text("Hello")
.padding()
}
}
Misconfiguration:
The view modifier .confirmationDialog needs to be added to the view that is outside of the .swipeActions view modifier. It works when configured properly as shown below:
import SwiftUI
struct ContentView: View {
#State private var confirmDelete = false
var body: some View {
NavigationView {
List{
ForEach(1..<10) {_ in
Cell()
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
confirmDelete.toggle()
} label: {
Label("Delete", systemImage: "trash")
}
}
//move outside the scope of the .swipeActions view modifier:
.confirmationDialog("Remove this?", isPresented: $confirmDelete) {
Button(role: .destructive) {
print("Removed!")
} label: {
Text("Yes, Remove this")
}
}
}
}
}
}
struct Cell: View {
var body: some View {
Text("Hello")
.padding()
}
}

SwiftUI NavigationLink in the TitleBar pushes the view twice, a Button doing the same thing is not

This is annoying. The Edit button in the NavigationBar pushes the View twice. I made a test button which behaves correctly doing the same thing:
import SwiftUI
struct DetailListPage: View {
#Environment(\.presentationMode) var presentationMode
var listName: ListNames
// #State private var isEditDetailListPageShowing = false
#State private var selection: String? = nil
var body: some View {
Form {
Section(header: Text(listName.title ?? "")
.font(.title)
.padding()) {
Text(listName.listDetail ?? "Nothing is set yet!")
.multilineTextAlignment(.leading)
.padding()
.cornerRadius(12)
}
NavigationLink(destination: EditDetailListPage(listName: listName)) {
Button {
} label: {
Text("Edit Page")
}
}
}
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
self.presentationMode.wrappedValue.dismiss()
} label: {
Text("Cancel")
} .padding()
//Edit List Detail
}
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(
destination: EditDetailListPage(listName: listName)) {
Text("Edit")
}
}
}
}
The Text("Edit") right above is pushing the view twice.
The Button above it acts correctly. Would like to use the navigationbaritem instead of the button.
Works well for me, on macos 11.4, xcode 12.5, target ios 14.5 and macCatalyst 11.3.
Probably some other code (or settings/system) that is causing the issue.
What system are you using? Show us the missing code and how you call the views. Let us know if the test code below does not work for you.
This is the test code I used:
#main
struct TestErrorApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct EditDetailListPage: View {
var listName: [String]
var body: some View {
Text("EditDetailListPage")
}
}
struct ContentView: View {
var body: some View {
NavigationView {
DetailListPage(listName: ["test var"])
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DetailListPage: View {
#Environment(\.presentationMode) var presentationMode
var listName: [String]
// #State private var isEditDetailListPageShowing = false
#State private var selection: String? = nil
var body: some View {
Form {
Section(header: Text("header string")
.font(.title)
.padding()) {
Text("Nothing is set yet!")
.multilineTextAlignment(.leading)
.padding()
.cornerRadius(12)
}
NavigationLink(destination: EditDetailListPage(listName: listName)) {
Button {
} label: {
Text("Edit Page")
}
}
}
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
self.presentationMode.wrappedValue.dismiss()
} label: {
Text("Cancel")
} .padding()
//Edit List Detail
}
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(
destination: EditDetailListPage(listName: listName)) {
Text("Edit")
}
}
}
}
}
struct ListFrontPage: View {
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) var managedObjectContext
#State private var isAddNewListShowing = false
var listNames: FetchRequest<ListNames>
init() {
listNames = FetchRequest<ListNames>(entity: ListNames.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \ListNames.sort, ascending: true)], animation: .default)
}
var body: some View {
VStack {
List() {
Text("Accounts")
.frame(maxWidth: .infinity)
.font(.system(size: 30, weight: .heavy, design: .default))
ForEach (listNames.wrappedValue) { listName in
NavigationLink(destination: DetailListPage(listName: listName)) {
Text("\(listName.title ?? "")")
}
}
.onDelete(perform: deleteItems)
.onMove(perform: moveItem)
}
Spacer()
ZStack {
NavigationLink(destination: AddNewList()) {
Image(systemName: "plus.circle.fill").font(.system(size: 64))
}
}
}
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button {
self.presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "chevron.backward")
Label("Back", image: "")
}, trailing: EditButton())
.navigationBarTitle(Text("Account Management"))
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { listNames.wrappedValue[$0] }.forEach(CoreDataHelper.sharedManager.deleteItems)
}
}
private func moveItem(from offsets: IndexSet, to destination: Int)
{
print("From: \(String(describing: offsets)) To: \(destination)")
// Make an array of items from fetched results
var revisedItems: [ ListNames ] = listNames.wrappedValue.map{ $0 }
// change the order of the items in the array
revisedItems.move(fromOffsets: offsets, toOffset: destination )
// update the userOrder attribute in revisedItems to
// persist the new order. This is done in reverse order
// to minimize changes to the indices.
for reverseIndex in stride( from: revisedItems.count - 1,
through: 0,
by: -1 )
{
revisedItems[ reverseIndex ].sort =
Int16( reverseIndex )
}
}
}

How we can adding a search bar with side bar icon to the navigation view?

I want to add a search bar to the navigation bar, but I do not know how to use search bar with sidebar icon in the same HStack. I put example screenshot with ContentView code. Any help would be appreciated.
Screenshot:
ContentView:
struct ContentView: View {
#State private var isShowing = false
var body: some View {
ZStack {
if isShowing {
SideMenuView(isShowing: $isShowing)
}
TabView {
NavigationView {
HomeView()
.navigationBarItems(leading: Button(action: {
withAnimation(.spring()) {
isShowing.toggle()
}
} , label: {
Image(systemName: "list.bullet")
}))
}
.tabItem {
Image(systemName: "1.circle")
Text("Page 1")
}
NavigationView {
HomeTwoView()
.navigationBarItems(leading: Button(action: {
withAnimation(.spring()) {
isShowing.toggle()
}
} , label: {
Image(systemName: "list.bullet")
}))
}
.tabItem {
Image(systemName: "2.circle")
Text("Page 2")
}
}
.edgesIgnoringSafeArea(.bottom)
//.cornerRadius(isShowing ? 20 : 0) //<< disabled due to strange effect
.offset(x: isShowing ? 300 : 0, y: isShowing ? 44: 0)
.scaleEffect(isShowing ? 0.8 : 1)
}.onAppear {
isShowing=false
}
}
}
As I mentioned in comments this is not possible in SwiftUI (2.0) yet. What you can do is integrating with UIKit.
Integrate with UIKit
class UIKitSearchBar: NSObject, ObservableObject {
#Published var text: String = ""
let searchController = UISearchController(searchResultsController: nil)
override init() {
super.init()
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.definesPresentationContext = true
self.searchController.searchResultsUpdater = self
}
}
extension UIKitSearchBar: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
// Publish search bar text changes.
if let searchBarText = searchController.searchBar.text {
self.text = searchBarText
}
}
}
struct SearchBarModifier: ViewModifier {
let searchBar: UIKitSearchBar
func body(content: Content) -> some View {
content
.overlay(
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
}
.frame(width: 0, height: 0)
)
}
}
extension View {
func add(_ searchBar: UIKitSearchBar) -> some View {
return self.modifier(SearchBarModifier(searchBar: searchBar))
}
}
final class ViewControllerResolver: UIViewControllerRepresentable {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
}
func makeUIViewController(context: Context) -> ParentResolverViewController {
ParentResolverViewController(onResolve: onResolve)
}
func updateUIViewController(_ uiViewController: ParentResolverViewController, context: Context) { }
}
class ParentResolverViewController: UIViewController {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
super.init(nibName: nil, bundle: nil)
}
#available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
if let parent = parent {
onResolve(parent)
}
}
}
Usage
struct Example: View {
#StateObject var searchBar = UIKitSearchBar()
var body: some View {
NavigationView {
Text("Example")
.add(searchBar)
.navigationTitle("Example")
}
}
}
In my own project I am using computed property to filter stuff, it can be helpful for you too. Here is my code:
var filteredExams: [Exam] {
examModel.exams.filter({ searchBar.text.isEmpty || $0.examName.localizedStandardContains(searchBar.text)})
}
Screenshot