Get PDF-Scanner in fullscreen in a sheet in SwiftUI - swiftui

I am working on a PDF-Scanner and want to realize it with SwiftUI. I know that I have to work with UIViewRepresentable to get this done.
So the user should open the Camera (to scan the document) in a Sheetview, but the camera doesn't open. If I put it in a Navigationview, it works fine.
My questions are:
Is it possible to open the camera in Sheetviews (fullscreen). Or is it possible put the CameraView in a limited frame in the SheetView to get this done.
Thanks a lot
import SwiftUI
import VisionKit
import PDFKit
import UIKit
#main
struct testetApp: App {
var body: some Scene {
WindowGroup {
ContentView(scannerModel: ScannerModel())
}
}
}
struct ContentView: View {
#State var showNextView = false
#StateObject var vm = CoreDataRelationshipViewModel()
#ObservedObject var scannerModel: ScannerModel
var body: some View {
NavigationView {
VStack {
Button {
showNextView.toggle()
} label: {
Text("via Sheet")
}.sheet(isPresented: $showNextView) {
ContentView2(scannerModel: scannerModel, showNextView: $showNextView).environment(\.managedObjectContext, vm.mangager.container.viewContext)
}.padding()
NavigationLink("via Navigationlink", destination: ContentView3(scannerModel: scannerModel))
}
.padding()
}
}
}
struct ContentView2: View {
#StateObject var vm = CoreDataRelationshipViewModel()
#ObservedObject var scannerModel: ScannerModel
#State var files : [String] = []
#State var PDFview = false
#State var addDoc = true
#Binding var showNextView: Bool
var body: some View {
ZStack{
NavigationView{
VStack{
VStack {
NavigationLink(destination: ScanView(files: $files, scannerModel: scannerModel, vm: vm)){
VStack{
Image(systemName: "plus").font(.largeTitle).padding(.bottom)
Text("Scan Document").font(.caption)
}
}
}
}
}
}
}
}
struct ContentView3: View {
#StateObject var vm = CoreDataRelationshipViewModel()
#ObservedObject var scannerModel: ScannerModel
#State var files : [String] = []
#State var PDFview = false
#State var addDoc = true
var body: some View {
ZStack{
VStack{
VStack {
NavigationLink(destination: ScanView(files: $files, scannerModel: scannerModel, vm: vm)){
VStack{
Image(systemName: "plus").font(.largeTitle).padding(.bottom)
Text("Scan Document").font(.caption)
}
}
}
}
}
}
}
struct ScanView: View{
#Environment(\.presentationMode) var mode
#State var pdfName = ""
#State var addDoc = true
#Binding var files : [String]
#ObservedObject var scannerModel: ScannerModel
#ObservedObject var vm: CoreDataRelationshipViewModel
var body: some View{
ZStack{
VStack{
if let error = scannerModel.errorMessage {
Text(error)
} else {
ForEach(scannerModel.imageArray, id: \.self) { image in
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit).contextMenu {
Button {
let items = [image]
let ac = UIActivityViewController(activityItems: items, applicationActivities: nil)
UIApplication.shared.windows.filter({$0.isKeyWindow}).first?.rootViewController?.present(ac, animated: true)
} label: {
Label("share Document", systemImage: "square.and.arrow.up")
}
Divider()
Button {
scannerModel.removeImage(image: image)
} label: {
Label("delete document", systemImage: "delete.left")
}
}
}
}
}.navigationBarItems( trailing: Button(action:{
vm.createSavedPDF(a: scannerModel.imageArray)
guard pdfName.count > 0 else{
return
}
self.mode.wrappedValue.dismiss()
saveDocument(a: scannerModel.imageArray, pdfName: pdfName)
scannerModel.imageArray.removeAll()
files = getDocumentsDirectory()
}){
Text("Save")
})
if(addDoc){
VStack{
VStack{
Button(action: {
UIApplication.shared.windows.filter({$0.isKeyWindow}).first?.rootViewController?.present(scannerModel.getDocumentCameraViewController(), animated: true, completion: nil)
addDoc = false
}){
VStack {
Image(systemName: "camera").font(.title)
Text("Scan Doc").font(.title)
}
}
}.padding()
}
}
}
}
}
struct PDFKitRepresentedView: UIViewRepresentable {
let url: URL
init(_ url: URL) {
self.url = url
}
func makeUIView(context: UIViewRepresentableContext<PDFKitRepresentedView>) -> PDFKitRepresentedView.UIViewType {
let pdfView = PDFView()
pdfView.document = PDFDocument(url: self.url)
pdfView.pageBreakMargins = UIEdgeInsets(top: 50, left: 30, bottom: 50, right:30)
pdfView.autoScales = true
return pdfView
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PDFKitRepresentedView>) {
}
}
struct ActivityViewController: UIViewControllerRepresentable {
var activityItems: [Any]
var applicationActivities: [UIActivity]? = nil
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}
}

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

Best library for image cropping in SwiftUI iOS?

I am working on iOS SwiftUI app Image editor, for image cropping I used the Mantis library but I faced one problem , when I pick image from gallery its working perfectly but getting app crashed when image is picked through camera .Getting this error "Message from debugger: Terminated due to memory issue"
Mantis library install link through package manager
Other library I found is QCropper but it does not have any documentation for SwiftUI
My Code :
HomePage
import SwiftUI
import Mantis
struct HomePage: View {
#State var bgImage :UIImage?
#State var camIsClicked = false
#State private var isImagePickerDisplay = false
#State var sourceType: UIImagePickerController.SourceType?
#State var showCropper = false
#State var gotoImageEdit = false
#State private var cropShapeType: Mantis.CropShapeType = .rect
#State private var presetFixedRatioType: Mantis.PresetFixedRatioType = .canUseMultiplePresetFixedRatio()
var camAlertView:some View{
VStack {
VStack{
VStack(spacing:Constants.device == .pad ? 20:10){
Text("Select One!")
.foregroundColor(Color.red)
.fontWeight(.bold)
.font(Constants.device == .pad ? .largeTitle:.title2)
Divider()
.frame(width:Constants.width*0.65 , height:Constants.device == .pad ? 3.5:2)
.background(Color.red)
}
.padding(.top)
Spacer()
Button {
sourceType = .photoLibrary
isImagePickerDisplay.toggle()
} label: {
Text("Gallery")
.fontWeight(.bold)
.font(Constants.device == .pad ? .title:.title3)
.frame(width:Constants.width*0.34, height:Constants.device == .pad ? 70:40)
.background(Color.red)
.cornerRadius(Constants.device == .pad ? 35:20)
}
Spacer()
Button {
sourceType = .camera
isImagePickerDisplay.toggle()
} label: {
Text("Camera")
.fontWeight(.bold)
.font(Constants.device == .pad ? .title:.title3)
.frame(width:Constants.width*0.34, height:Constants.device == .pad ? 70:40)
.background(Color.red)
.cornerRadius(Constants.device == .pad ? 35:20)
}
Spacer()
}
.frame(width:Constants.width*0.65, height:Constants.height*0.39)
.background(Color.white)
.cornerRadius(20)
.foregroundColor(.white)
}
.frame(width:Constants.width, height:Constants.height)
.background(Color.black
.opacity(0.8)
.ignoresSafeArea()
.onTapGesture {
camIsClicked = false
})
}
var body: some View {
ZStack{
NavigationLink( destination: ImageEditPage(bgImage: $bgImage),isActive: $gotoImageEdit) {
EmptyView()
}
VStack{
Button {
camIsClicked.toggle()
} label: {
Text("Pick Image")
.padding()
.background(Color.green)
.cornerRadius(30)
}
}
if camIsClicked{
camAlertView
}
if isImagePickerDisplay{
if sourceType == .photoLibrary{
SUImagePickerView(sourceType: .photoLibrary, image: $bgImage, isPresented: $isImagePickerDisplay, camIsClicked: $camIsClicked, bgImageIsSelected: $showCropper)
}else{
SUImagePickerView(sourceType: .camera, image: $bgImage, isPresented: $isImagePickerDisplay, camIsClicked: $camIsClicked, bgImageIsSelected: $showCropper)
}
}
}
.navigationBarHidden(true)
.fullScreenCover(isPresented: $showCropper, content: {
ImageCropper(gotoImageEdit: $gotoImageEdit, image: $bgImage,cropShapeType: $cropShapeType,presetFixedRatioType: $presetFixedRatioType)
.ignoresSafeArea()
})
}
}
struct HomePage_Previews: PreviewProvider {
static var previews: some View {
HomePage()
}
}
ImageCropper Class
import SwiftUI
import Mantis
struct ImageCropper: UIViewControllerRepresentable {
#Binding var gotoImageEdit : Bool
#Binding var image: UIImage?
#Binding var cropShapeType: Mantis.CropShapeType
#Binding var presetFixedRatioType: Mantis.PresetFixedRatioType
#Environment(\.presentationMode) var presentationMode
class Coordinator: CropViewControllerDelegate {
func cropViewControllerDidImageTransformed(_ cropViewController: CropViewController) {
}
var parent: ImageCropper
init(_ parent: ImageCropper) {
self.parent = parent
}
func cropViewControllerDidCrop(_ cropViewController: CropViewController, cropped: UIImage, transformation: Transformation, cropInfo: CropInfo) {
parent.image = cropped
print("transformation is \(transformation)")
parent.gotoImageEdit = true
parent.presentationMode.wrappedValue.dismiss()
}
func cropViewControllerDidCancel(_ cropViewController: CropViewController, original: UIImage) {
parent.presentationMode.wrappedValue.dismiss()
}
func cropViewControllerDidFailToCrop(_ cropViewController: CropViewController, original: UIImage) {
}
func cropViewControllerDidBeginResize(_ cropViewController: CropViewController) {
}
func cropViewControllerDidEndResize(_ cropViewController: CropViewController, original: UIImage, cropInfo: CropInfo) {
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> CropViewController {
var config = Mantis.Config()
config.cropViewConfig.cropShapeType = cropShapeType
config.presetFixedRatioType = presetFixedRatioType
let cropViewController = Mantis.cropViewController(image: image!,
config: config)
cropViewController.delegate = context.coordinator
return cropViewController
}
func updateUIViewController(_ uiViewController: CropViewController, context: Context) {
}
}
I think it is a bug in the last version of the mantis library. I had the same problem. I installed version 2.1.2 and it seems to work. Your code looks fine.

It does not read that the setting has changed

I have a problem I change the "isDynamic" setting in the "SettingView" and I exit the setting window and the "SongbookView" does not register that the setting has changed. I want to change the search engine depending on what option is selected in the settings. What is the cause of this situation?
SongbookView:
import CoreData
import SwiftUI
struct SongbookView: View {
#State var searchText: String = ""
#State var isSettings: Bool
#ObservedObject var userSettings: UserSettings = UserSettings()
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: Song.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Song.number, ascending: true)]
) var songs: FetchedResults<Song>
var body: some View {
NavigationView{
VStack{
if userSettings.isDynamic == false {
SearchBar(text: $searchText)
} else {
DynamicSearchBar(text: $searchText)
}
List(songs.filter({searchText.isEmpty ? true : removeNumber(str: $0.content!.lowercased()).contains(searchText.lowercased()) || String($0.number).contains(searchText)}), id:\.objectID) { song in
NavigationLink(destination: DetailView(song: song, isSelected: song.favorite)) {
HStack{
Text("\(String(song.number)). ") .font(.headline) + Text(song.title ?? "Brak tytułu")
if song.favorite {
Spacer()
Image(systemName: "heart.fill")
.accessibility(label: Text("To jest ulubiona pieśń"))
.foregroundColor(.red)
}
}.lineLimit(1)
}
}.id(UUID())
.listStyle(InsetListStyle())
}
.padding(.top, 10)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
HStack{
Text(String(self.userSettings.isDynamic))
Spacer()
Text("Śpiewnik")
.font(.system(size: 20))
.bold()
Spacer()
Button(action: {
isSettings.toggle()
print(userSettings.isDynamic)
}) {
Image(systemName: "gearshape")
.resizable()
.frame(width: 16.0, height: 16.0)
}
.sheet(isPresented: $isSettings) {
SettingView(isPresented: $isSettings)
}
}
}
}
}
}
func removeNumber(str: String) -> String {
var result = str
let vowels: Set<Character> = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
result.removeAll(where: { vowels.contains($0) })
return result
}
}
SettingView:
import SwiftUI
struct SettingView: View {
#ObservedObject var userSettings = UserSettings()
#Binding var isPresented: Bool
var body: some View {
NavigationView {
Form {
Toggle("Dynamiczna wyszukiwarka", isOn: $userSettings.isDynamic)
.onChange(of: userSettings.isDynamic) { value in
print(value)
}
Button(action: {
print(userSettings.isDynamic)
isPresented = false
}) {
Text("Test")
}
}
.navigationBarTitle("Ustawienia")
}
}
}
UserSettings:
import Foundation
import Combine
class UserSettings: ObservableObject {
#Published var isDynamic: Bool {
didSet {
UserDefaults.standard.set(isDynamic, forKey: "isSearchDynamic")
}
}
init() {
self.isDynamic = UserDefaults.standard.object(forKey: "isSearchDynamic") as? Bool ?? false
}
}
You're using two different instances of UserSettings. When you update isDynamic on one of those instances, the other, even though it has a reference to UserDefaults has no reason to know that it needs to update.
The easiest solution here is to share a single instance of UserSettings:
struct SongbookView: View {
#State var searchText: String = ""
#State var isSettings: Bool
#ObservedObject var userSettings: UserSettings = UserSettings()
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: Song.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Song.number, ascending: true)]
) var songs: FetchedResults<Song>
var body: some View {
//...
.sheet(isPresented: $isSettings) {
SettingView(userSettings: userSettings,isPresented: $isSettings)
}
//...
}
}
struct SettingView: View {
#ObservedObject var userSettings : UserSettings
#Binding var isPresented: Bool
var body: some View {
//...
}
}
You could also look into property wrappers like #AppStorage (https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-appstorage-property-wrapper) that actually update dynamically when the UserDefaults values change.

TabView SwiftUI return to Home page on click [duplicate]

Starting point is a NavigationView within a TabView. I'm struggling with finding a SwiftUI solution to pop to the root view within the navigation stack when the selected tab is tapped again. In the pre-SwiftUI times, this was as simple as the following:
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let navController = viewController as! UINavigationController
navController.popViewController(animated: true)
}
Do you know how the same thing can be achieved in SwiftUI?
Currently, I use the following workaround that relies on UIKit:
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let navigationController = UINavigationController(rootViewController: UIHostingController(rootView: MyCustomView() // -> this is a normal SwiftUI file
.environment(\.managedObjectContext, context)))
navigationController.tabBarItem = UITabBarItem(title: "My View 1", image: nil, selectedImage: nil)
// add more controllers that are part of tab bar controller
let tabBarController = UITabBarController()
tabBarController.viewControllers = [navigationController /* , additional controllers */ ]
window.rootViewController = tabBarController // UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
Here is possible approach. For TabView it gives the same behaviour as tapping to the another tab and back, so gives persistent look & feel.
Tested & works with Xcode 11.2 / iOS 13.2
Full module code:
import SwiftUI
struct TestPopToRootInTab: View {
#State private var selection = 0
#State private var resetNavigationID = UUID()
var body: some View {
let selectable = Binding( // << proxy binding to catch tab tap
get: { self.selection },
set: { self.selection = $0
// set new ID to recreate NavigationView, so put it
// in root state, same as is on change tab and back
self.resetNavigationID = UUID()
})
return TabView(selection: selectable) {
self.tab1()
.tabItem {
Image(systemName: "1.circle")
}.tag(0)
self.tab2()
.tabItem {
Image(systemName: "2.circle")
}.tag(1)
}
}
private func tab1() -> some View {
NavigationView {
NavigationLink(destination: TabChildView()) {
Text("Tab1 - Initial")
}
}.id(self.resetNavigationID) // << making id modifiable
}
private func tab2() -> some View {
Text("Tab2")
}
}
struct TabChildView: View {
var number = 1
var body: some View {
NavigationLink("Child \(number)",
destination: TabChildView(number: number + 1))
}
}
struct TestPopToRootInTab_Previews: PreviewProvider {
static var previews: some View {
TestPopToRootInTab()
}
}
Here's an approach that uses a PassthroughSubject to notify the child view whenever the tab is re-selected, and a view modifier to allow you to attach .onReselect() to a view.
import SwiftUI
import Combine
enum TabSelection: String {
case A, B, C // etc
}
private struct DidReselectTabKey: EnvironmentKey {
static let defaultValue: AnyPublisher<TabSelection, Never> = Just(.Mood).eraseToAnyPublisher()
}
private struct CurrentTabSelection: EnvironmentKey {
static let defaultValue: Binding<TabSelection> = .constant(.Mood)
}
private extension EnvironmentValues {
var tabSelection: Binding<TabSelection> {
get {
return self[CurrentTabSelection.self]
}
set {
self[CurrentTabSelection.self] = newValue
}
}
var didReselectTab: AnyPublisher<TabSelection, Never> {
get {
return self[DidReselectTabKey.self]
}
set {
self[DidReselectTabKey.self] = newValue
}
}
}
private struct ReselectTabViewModifier: ViewModifier {
#Environment(\.didReselectTab) private var didReselectTab
#State var isVisible = false
let action: (() -> Void)?
init(perform action: (() -> Void)? = nil) {
self.action = action
}
func body(content: Content) -> some View {
content
.onAppear {
self.isVisible = true
}.onDisappear {
self.isVisible = false
}.onReceive(didReselectTab) { _ in
if self.isVisible, let action = self.action {
action()
}
}
}
}
extension View {
public func onReselect(perform action: (() -> Void)? = nil) -> some View {
return self.modifier(ReselectTabViewModifier(perform: action))
}
}
struct NavigableTabViewItem<Content: View>: View {
#Environment(\.didReselectTab) var didReselectTab
let tabSelection: TabSelection
let imageName: String
let content: Content
init(tabSelection: TabSelection, imageName: String, #ViewBuilder content: () -> Content) {
self.tabSelection = tabSelection
self.imageName = imageName
self.content = content()
}
var body: some View {
let didReselectThisTab = didReselectTab.filter( { $0 == tabSelection }).eraseToAnyPublisher()
NavigationView {
self.content
.navigationBarTitle(tabSelection.localizedStringKey, displayMode: .inline)
}.tabItem {
Image(systemName: imageName)
Text(tabSelection.localizedStringKey)
}
.tag(tabSelection)
.navigationViewStyle(StackNavigationViewStyle())
.keyboardShortcut(tabSelection.keyboardShortcut)
.environment(\.didReselectTab, didReselectThisTab)
}
}
struct NavigableTabView<Content: View>: View {
#State private var didReselectTab = PassthroughSubject<TabSelection, Never>()
#State private var _selection: TabSelection = .Mood
let content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
let selection = Binding(get: { self._selection },
set: {
if self._selection == $0 {
didReselectTab.send($0)
}
self._selection = $0
})
TabView(selection: selection) {
self.content
.environment(\.tabSelection, selection)
.environment(\.didReselectTab, didReselectTab.eraseToAnyPublisher())
}
}
}
Here's how I did it:
struct UIKitTabView: View {
var viewControllers: [UIHostingController<AnyView>]
init(_ tabs: [Tab]) {
self.viewControllers = tabs.map {
let host = UIHostingController(rootView: $0.view)
host.tabBarItem = $0.barItem
return host
}
}
var body: some View {
TabBarController(controllers: viewControllers).edgesIgnoringSafeArea(.all)
}
struct Tab {
var view: AnyView
var barItem: UITabBarItem
init<V: View>(view: V, barItem: UITabBarItem) {
self.view = AnyView(view)
self.barItem = barItem
}
}
}
struct TabBarController: UIViewControllerRepresentable {
var controllers: [UIViewController]
func makeUIViewController(context: Context) -> UITabBarController {
let tabBarController = UITabBarController()
tabBarController.viewControllers = controllers
tabBarController.delegate = context.coordinator
return tabBarController
}
func updateUIViewController(_ uiViewController: UITabBarController, context: Context) { }
}
extension TabBarController {
func makeCoordinator() -> TabBarController.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITabBarControllerDelegate {
var parent: TabBarController
init(_ parent: TabBarController){self.parent = parent}
var previousController: UIViewController?
private var shouldSelectIndex = -1
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
shouldSelectIndex = tabBarController.selectedIndex
return true
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if shouldSelectIndex == tabBarController.selectedIndex {
if let navVC = tabBarController.viewControllers![shouldSelectIndex].nearestNavigationController {
if (!(navVC.popViewController(animated: true) != nil)) {
navVC.viewControllers.first!.scrollToTop()
}
}
}
}
}
}
extension UIViewController {
var nearestNavigationController: UINavigationController? {
if let selfTypeCast = self as? UINavigationController {
return selfTypeCast
}
if children.isEmpty {
return nil
}
for child in self.children {
return child.nearestNavigationController
}
return nil
}
}
extension UIViewController {
func scrollToTop() {
func scrollToTop(view: UIView?) {
guard let view = view else { return }
switch view {
case let scrollView as UIScrollView:
if scrollView.scrollsToTop == true {
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.safeAreaInsets.top), animated: true)
return
}
default:
break
}
for subView in view.subviews {
scrollToTop(view: subView)
}
}
scrollToTop(view: view)
}
}
Then in ContentView.swift I use it like this:
struct ContentView: View {
var body: some View {
ZStack{
UIKitTabView([
UIKitTabView.Tab(
view: FirstView().edgesIgnoringSafeArea(.top),
barItem: UITabBarItem(title: "Tab1", image: UIImage(systemName: "star"), selectedImage: UIImage(systemName: "star.fill"))
),
UIKitTabView.Tab(
view: SecondView().edgesIgnoringSafeArea(.top),
barItem: UITabBarItem(title: "Tab2", image: UIImage(systemName: "star"), selectedImage: UIImage(systemName: "star.fill"))
),
])
}
}
}
Note that when the user is already on the root view, it scrolls to top automatically
Here's what I did with introspect swiftUI library.
https://github.com/siteline/SwiftUI-Introspect
struct TabBar: View {
#State var tabSelected: Int = 0
#State var navBarOne: UINavigationController?
#State var navBarTwo: UINavigationController?
#State var navBarThree: UINavigationController?
var body: some View {
return TabView(selection: $tabSelected){
NavView(navigationView: $navBarOne).tabItem {
Label("Home1",systemImage: "bag.fill")
}.tag(0)
NavView(navigationView: $navBarTwo).tabItem {
Label("Orders",systemImage: "scroll.fill" )
}.tag(1)
NavView(navigationView: $navBarThree).tabItem {
Label("Wallet", systemImage: "dollarsign.square.fill" )
// Image(systemName: tabSelected == 2 ? "dollarsign.square.fill" : "dollarsign.square")
}.tag(2)
}.onTapGesture(count: 2) {
switch tabSelected{
case 0:
self.navBarOne?.popToRootViewController(animated: true)
case 1:
self.navBarTwo?.popToRootViewController(animated: true)
case 2:
self.navBarThree?.popToRootViewController(animated: true)
default:
print("tapped")
}
}
}
}
NavView:
import SwiftUI
import Introspect
struct NavView: View {
#Binding var navigationView: UINavigationController?
var body: some View {
NavigationView{
VStack{
NavigationLink(destination: Text("Detail view")) {
Text("Go To detail")
}
}.introspectNavigationController { navController in
navigationView = navController
}
}
}
}
This actually isn't the best approach because it makes the entire tab view and everything inside of it have the double-tap gesture which would pop the view to its root. My current fix for this allows for one tap to pop up root view haven't figured out how to add double tap
struct TabBar: View {
#State var tabSelected: Int = 0
#State var navBarOne: UINavigationController?
#State var navBarTwo: UINavigationController?
#State var navBarThree: UINavigationController?
#State var selectedIndex:Int = 0
var selectionBinding: Binding<Int> { Binding(
get: {
self.selectedIndex
},
set: {
if $0 == self.selectedIndex {
popToRootView(tabSelected: $0)
}
self.selectedIndex = $0
}
)}
var body: some View {
return TabView(selection: $tabSelected){
NavView(navigationView: $navBarOne).tabItem {
Label("Home1",systemImage: "bag.fill")
}.tag(0)
NavView(navigationView: $navBarTwo).tabItem {
Label("Orders",systemImage: "scroll.fill" )
}.tag(1)
NavView(navigationView: $navBarThree).tabItem {
Label("Wallet", systemImage: "dollarsign.square.fill" )
// Image(systemName: tabSelected == 2 ? "dollarsign.square.fill" : "dollarsign.square")
}.tag(2)
}
}
func popToRootView(tabSelected: Int){
switch tabSelected{
case 0:
self.navBarOne?.popToRootViewController(animated: true)
case 1:
self.navBarTwo?.popToRootViewController(animated: true)
case 2:
self.navBarThree?.popToRootViewController(animated: true)
default:
print("tapped")
}
}
}
I took an approach similar to Asperi
Use a combination of a custom binding, and a separately stored app state var for keeping state of the navigation link.
The custom binding allows you to see all taps basically even when the current tab is the one thats tapped, something that onChange of tab selection binding doesn't show. This is what imitates the UIKit TabViewDelegate behavior.
This doesn't require a "double tap", if you just a single tap of the current, if you want double tap you'll need to implement your own tap/time tracking but shouldn't be too hard.
class AppState: ObservableObject {
#Published var mainViewShowingDetailView = false
}
struct ContentView: View {
#State var tabState: Int = 0
#StateObject var appState = AppState()
var body: some View {
let binding = Binding<Int>(get: { tabState },
set: { newValue in
if newValue == tabState { // tapped same tab they're already on
switch newValue {
case 0: appState.mainViewShowingDetailView = false
default: break
}
}
tabState = newValue // make sure you actually set the storage
})
TabView(selection: binding) {
MainView()
.tabItem({ Label("Home", systemImage: "list.dash") })
.tag(0)
.environmentObject(appState)
}
}
}
struct MainView: View {
#EnvironmentObject var appState: AppState
var body: {
NavigationView {
VStack {
Text("Hello World")
NavigationLink(destination: DetailView(),
isActive: $appState.mainViewShowingDetailView,
label: { Text("Show Detail") })
}
}
}
}
struct DetailView: View {
...
}
iOS 16 / NavigationStack approach with PassthroughSubject
Uses willSet on selectedTab to get the tap event, and uses a PassthroughSubject for sending the event to the children. This is picked up by the .onReceived and calls a function for popping the views from the NavigationStack
Did a full write up here: https://kentrobin.com/home/tap-tab-to-go-back/ and created a working demo project here: https://github.com/kentrh/demo-tap-tab-to-go-back
class HomeViewModel: ObservableObject {
#Published var selectedTab: Tab = .tab1 {
willSet {
if selectedTab == newValue {
subject.send(newValue)
}
}
}
let subject = PassthroughSubject<Tab, Never>()
enum Tab: Int {
case tab1 = 0
}
}
struct HomeView: View {
#StateObject var viewModel: HomeViewModel = .init()
var body: some View {
TabView(selection: $viewModel.selectedTab) {
Tab1View(subject: viewModel.subject)
.tag(HomeViewModel.Tab.tab1)
.tabItem {
Label("Tab 1", systemImage: "1.lane")
Text("Tab 1", comment: "Tab bar title")
}
}
}
}
struct Tab1View: View {
#StateObject var viewModel: Tab1ViewModel = .init()
let subject: PassthroughSubject<HomeViewModel.Tab, Never>
var body: some View {
NavigationStack(path: $viewModel.path) {
List {
NavigationLink(value: Tab1ViewModel.Route.viewOne("From tab 1")) {
Text("Go deeper to OneView")
}
NavigationLink(value: Tab1ViewModel.Route.viewTwo("From tab 1")) {
Text("Go deeper to TwoView")
}
}
.navigationTitle("Tab 1")
.navigationDestination(for: Tab1ViewModel.Route.self, destination: { route in
switch route {
case let .viewOne(text):
Text(text)
case let .viewTwo(text):
Text(text)
}
})
.onReceive(subject) { tab in
if case .tab1 = tab { viewModel.tabBarTapped() }
}
}
}
}
class Tab1ViewModel: ObservableObject {
#Published var path: [Route] = []
func tabBarTapped() {
if path.count > 0 {
path.removeAll()
}
}
enum Route: Hashable {
case viewOne(String)
case viewTwo(String)
}
}

SwiftUI How can I add an activity indicator in WKWebView?

How can I add an activity indicator in WKWebView which will display the indicator while the webpage is loading and disappears when loaded ?
I've looked at some of the old posts but could not figure out how to do it in SwiftUI
see link to one of the old solutions below
How to add Activity Indicator to WKWebView (Swift 3)
Use UIViewRepresentable to create a UIActivityIndicatorView:
You control when an activity indicator animates by calling the startAnimating() and stopAnimating() methods. To automatically hide the activity indicator when animation stops, set the hidesWhenStopped property to true.
You can set the color of the activity indicator by using the color property.
struct ActivityIndicatorView: UIViewRepresentable {
#Binding var isAnimating: Bool
let style: UIActivityIndicatorView.Style
func makeUIView(context: UIViewRepresentableContext<ActivityIndicatorView>) -> UIActivityIndicatorView {
return UIActivityIndicatorView(style: style)
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityIndicatorView>) {
isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
}
}
Create a LoadingView to allow you to wrap around your views:
This allows you to style the activity views content.
struct LoadingView<Content>: View where Content: View {
#Binding var isShowing: Bool
var content: () -> Content
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
self.content()
.disabled(self.isShowing)
.blur(radius: self.isShowing ? 3 : 0)
VStack {
Text("Loading...")
ActivityIndicatorView(isAnimating: .constant(true), style: .large)
}
.frame(width: geometry.size.width / 2, height: geometry.size.height / 5)
.background(Color.secondary.colorInvert())
.foregroundColor(Color.red)
.cornerRadius(20)
.opacity(self.isShowing ? 1 : 0)
}
}
}
}
If you want to be able to update the LoadingView(...) status you'll need to introduce a view model that inherits from ObservableObject:
Based on this answer: https://stackoverflow.com/a/58825642/264802
class WebViewModel: ObservableObject {
#Published var url: String
#Published var isLoading: Bool = true
init (url: String) {
self.url = url
}
}
struct WebView: UIViewRepresentable {
#ObservedObject var viewModel: WebViewModel
let webView = WKWebView()
func makeCoordinator() -> Coordinator {
Coordinator(self.viewModel)
}
class Coordinator: NSObject, WKNavigationDelegate {
private var viewModel: WebViewModel
init(_ viewModel: WebViewModel) {
self.viewModel = viewModel
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.viewModel.isLoading = false
}
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<WebView>) { }
func makeUIView(context: Context) -> UIView {
self.webView.navigationDelegate = context.coordinator
if let url = URL(string: self.viewModel.url) {
self.webView.load(URLRequest(url: url))
}
return self.webView
}
}
Then to use it inside your views you would do the following:
struct ContentView: View {
#StateObject var model = WebViewModel(url: "http://www.google.com")
var body: some View {
LoadingView(isShowing: self.$model.isLoading) {
WebView(viewModel: self.model)
}
}
}
Using 3 Steps I do it in my project.
Step 1: Create a Loading View
import SwiftUI
import UIKit
struct ActivityIndicatorView: UIViewRepresentable {
#Binding var isAnimating: Bool
let style: UIActivityIndicatorView.Style
func makeUIView(context: Context) -> UIActivityIndicatorView {
return UIActivityIndicatorView(style: style)
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) {
isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
}
}
// Main View
struct LoadingView<Content>: View where Content: View {
#Binding var isShowing: Bool
let message: String
var content: () -> Content
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
self.content()
.disabled(self.isShowing)
.blur(radius: self.isShowing ? 3 : 0)
VStack {
Text(self.message)
.bold()
ActivityIndicatorView(isAnimating: .constant(true), style: .large)
}
.frame(width: geometry.size.width / 2,
height: geometry.size.height / 5)
.background(Color.secondary.colorInvert())
.foregroundColor(Color.primary)
.cornerRadius(20)
.opacity(self.isShowing ? 1 : 0)
}
}
}
}
// Mark: Testing
struct LoadingIndicator: View {
var body: some View {
LoadingView(isShowing: .constant(true), message: "Loading...") {
NavigationView {
List(["1", "2", "3", "4", "5"], id: \.self) { row in
Text(row)
}.navigationBarTitle(Text("A List"), displayMode: .large)
}
}
}
}
struct ActivityIndicatorView_Previews: PreviewProvider {
static var previews: some View {
LoadingIndicator()
}
}
Step 2: Create a WebView and WebViewModel
import SwiftUI
import WebKit
class WebViewModel: ObservableObject {
#Published var isLoading: Bool = false
}
struct WebView: UIViewRepresentable {
#ObservedObject var webViewModel: WebViewModel
let urlString: String
func makeUIView(context: Context) -> WKWebView {
let wkWebView = WKWebView()
if let url = URL(string: urlString) {
let urlRequest = URLRequest(url: url)
wkWebView.load(urlRequest)
}
return wkWebView
}
func updateUIView(_ wkWebView: WKWebView, context: Context) {
// do nothing
}
class Coordinator: NSObject, WKNavigationDelegate {
let webViewModel: WebViewModel
init(_ webViewModel: WebViewModel) {
self.webViewModel = webViewModel
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
webViewModel.isLoading = true
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webViewModel.isLoading = false
}
}
func makeCoordinator() -> WebView.Coordinator {
Coordinator(webViewModel)
}
}
struct WebView_Previews: PreviewProvider {
static var previews: some View {
WebView(webViewModel: WebViewModel(),
urlString: "https://instagram.com/mahmudahsan/")
}
}
Step 3: In your main view use the following code to show indicator and webview
ZStack {
WebView(webViewModel: webViewModel, urlString: "http://ithinkdiff.net")
.frame(height: 1000)
if webViewModel.isLoading {
LoadingView(isShowing: .constant(true), message: "Loading...") {
EmptyView()
}
}
}