I want to push a UIImagePickerViewController with a button in my parent view controller, but I want to display the image in a different view, not the parent view. I've tried
Pushing a new view and calling the image picker from a button there. However, because I have this view embedded inside a navigation view, I have a problem of 2 navigation bars. I can't hide navigation bar because then, I can't go back to the parent view.
Pushing a new view and calling the image picker directly (with no button). However, the image picker is not closing on its own and I can't go back to my parent view controller.
How about something like this:
Have an observed object to be the main object (your enviornmentObject).
Add a UIImage property to it or any property you want to share between views (after all that's the job of an environmentObject
Share the enviornmentObject
This is your class
class AppState: ObservableObject {
#Published var selectedImage: UIImage? = nil // default it to nil in case nothing is selected
}
This is your main view
struct ContentView: View {
#EnviornmentObject var appState: AppState
#State var presentModal: Bool = false
var body: some View {
VStack {
// Image can now easily be accessed by calling self.appState.selectedImage in any view that has #EnviornmentObject var appState: AppState
if(self.appState.selectedImage != nil) {
Image(uiImage: self.appState.selectedImage!)
} else {
// Image doesn't exist, add a placeholder
Text("No image selected")
}
Button("Show Modal") {
self.presentModal.toggle()
}
}.sheet(isPresented: self.$presentModal) {
ModalView(presentModal: self.$presentModal)
}
}
}
// Your ImagePicker view or any other view that will change the selected Image
struct ModalView: View {
#EnviornmentObject var appState: AppState
#Binding var presentModal: Bool = false
var body: some View {
// Your logic to pick image goes here, I will simulate a button click
Button("I will set an image") {
self.appState.selectedImage = UIImage(named: "test.jpg")
self.presentModal.toggle()
}
}
}
In your SceneDelegate (VERY IMPORTANT)
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView().environmentObject(AppState()) // <- The important part
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
Your modal view (or your image picker view, really anything)
EDIT: I agree it should be presented as a modal; however, it is not 100% necessary. Presenting it as a modal or not this should work though.
As the docs for UIUmagePickerController will tell you, it must be presented modally, not pushed. You are also responsible for dismissing it via the delegate.
Related
I'm building an app for MacOS. The functionality requires the window to increase in size in certain situations and to scale back outside of these situations. Is this possible? any pointers or examples are appreciated
Yes, this is possible by updating the frame of the NSWindow for your application, using setFrame. This method allows for setting the new frame (position and size) with and/or without animation.
Without animation (see setFrame(NSRect, display: Bool))
window.setFrame(newFrame, display: true)
With animation (see setFrame(NSRect, display: Bool, animate: Bool))
window.setFrame(newFrame, display: true, animated: true)
Calling any of these two methods will update the window of your application to whatever new frame you pass, including position and size.
The tricky part in SwiftUI is to get a hold to the NSWindow, since it is not trivial. Here is the TLDR:
Create a NSViewRepresentable to access the backing window:
struct WindowAccessor: NSViewRepresentable {
#Binding var window: NSWindow?
func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
self.window = view.window
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
}
The window is set in the next run loop using dispatch async, since it would be nil beforehand.
Add a window property to your SwiftUI view, and use the window accessor with a background modifier.
struct MyView: View {
#State private var window: NSWindow?
var body: some View {
VStack {
// Your view content. Note: window might be nil until set
}
.background(WindowAccessor(window: $window))
.onChange(of: window) {
// This method will get called once when the window is set
}
}
}
The Home app on iPadOS 14 displays black text on the left side of the status bar, and white text on the right side. How is this achieved? Can it be done via public APIs?
Can it be done via public APIs?
Yes, it's all defined in UIBarStyle. Since each uiviewcontroller added to a splitview is embedded into its own UINavigationController, you can set the barstyle for each uiviewscontroller.navigationController!.navigationBar.barStyle
For white text, choose .black, and .default is black text.
myHomeViewController.navigationController!.navigationBar.barStyle = .black
you could also create your own viewcontrollers and define they're preferredstatusbarstyle to be .lightContent or .darkContent by overriding the childForStatusBarStyle as per the docs.
You can override the preferred status bar style for a view controller
by implementing the childForStatusBarStyle method.
As per Apples wwdc20 Build for iPad. Home was rebuilt using the new features of UISplitViewController.
Minus the button and fancy background. Home split view is a UISplitViewController initialized with the doubleColumn style. UISplitViewController(style: .doubleColumn)
Example of creating a split view in code only in SceneDelegate.swift of a new project:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
//UISplitViewControllerDelegate
splitViewController.delegate = self
//set which view is what
splitViewController.setViewController(sidebarViewController, for: .primary)
splitViewController.setViewController(myHomeViewController, for: .secondary)
//setup of sidebar and detail controllers
sidebarViewController.navigationController?.navigationBar.prefersLargeTitles = true
myHomeViewController.navigationController?.navigationBar.prefersLargeTitles = true
sidebarViewController.title = "Home"
myHomeViewController.title = "Home"
//appear side by side when a column is shown, use tile style.
splitViewController.preferredSplitBehavior = .tile
//'color' myHomeViewController statusbar to white, by setting barStyle = .black
myHomeViewController.navigationController!.navigationBar.barStyle = .black
//start styling your navigation controllers, As i've set preferesLargeTitles, style the navBars largeTitleTextAttributes
myHomeViewController.navigationController!.navigationBar.largeTitleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
//style viewcontroller
myHomeViewController.view.backgroundColor = .blue
// always have primary and secondary sidebyside
//splitViewController.preferredDisplayMode = .oneBesideSecondary
splitViewController.show(.primary)
splitViewController.show(.secondary)
window = UIWindow(windowScene: windowScene)
window?.rootViewController = splitViewController
window?.makeKeyAndVisible()
}
Other helpful resources I found:
TLDW wwdc20 notes
UISplitViewController apple docs
This can be done only if the root view controller is a UISplitViewController. I've set one up with the new splitview column styles in iOS 14, but the older way should work too.
Then you need to get the navigationController from each viewController and set the barStyle on its navigationBar to .default or .black
After placing your splitViewController as the root viewController of your UIWindow in your scene function, set the style as below:
var embeddedSplitViewController: UISplitViewController?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
self.makeSplitViewController()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = self.embeddedSplitViewController
self.window = window
window.makeKeyAndVisible()
self.embeddedSplitViewController?.viewController(for: .primary)?.navigationController?.navigationBar.barStyle = .black
self.embeddedSplitViewController?.viewController(for: .secondary)?.navigationController?.navigationBar.barStyle = .default
}
}
func makeSplitViewController() {
let splitViewController = UISplitViewController(style: .doubleColumn)
// I have SwiftUI Views here but they could be any UIViewController
let primaryViewController = UIHostingController(rootView: SidebarView())
splitViewController.setViewController(primaryViewController, for: .primary)
splitViewController.setViewController(UIHostingController(rootView: DetailView()), for: .secondary)
let compactViewController = UIHostingController(rootView: SidebarView())
splitViewController.setViewController(compactViewController, for: .compact)
splitViewController.preferredPrimaryColumnWidth = 320
self.embeddedSplitViewController = splitViewController
}
This worked for me, and the statusBar reflected the barStyle and updated accordingly when showing and hiding the primary viewController.
I want to edit objects using popovers in my macOS application. But for some reason the popover does not appear anymore, when it was closed the popover while editing a TextField. (see gif bellow)
Any ideas, why this is happening?
Code:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
SubView()
SubView()
SubView()
}.padding()
}
}
struct SubView: View {
#State var showPopover = false
var body: some View {
VStack {
Text("Label")
}.onTapGesture {
self.showPopover = true
}
.popover(isPresented: $showPopover, arrowEdge: .trailing) {
Popover()
}
}
}
struct Popover: View {
#State var test: String = ""
var body: some View {
TextField("Text", text: $test)
}
}
It looks like it is not enough one event to resign editor first responder and close previous popover, so state of following popover is toggled, but new popover is not allowed, because previous is still on-screen.
The following workaround is possible (tested & works with Xcode 11.2)
}.onTapGesture {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
self.showPopover = true // delay activating new popover
}
}
Also it is possible to consider design approach when there is only one popover bindable to models of different subviews (which seems to me more appropriate) and manageable by the one state.
I have a view that opens another view Controller as popover
the popover uses an image picker and displays the chosen image in an imageView.
The parent also has an imageView with an IBoutlet named profiler and before dismissing the popover i would like the parent view to get the chosen image.
How would i go about accessing parent and sending the image from the popover to the parent view
below is code from the popover view controller:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
picker.delegate = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//MARK: - Delegates
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject])
{
let chosenImage = info[UIImagePickerControllerOriginalImage] as! UIImage //2
myImageView.contentMode = .scaleAspectFit //3
myImageView.image = chosenImage //4
myImageView.layer.borderWidth = 1
myImageView.layer.masksToBounds = false
myImageView.layer.borderColor = UIColor.black.cgColor
myImageView.layer.cornerRadius = myImageView.frame.height/4
myImageView.clipsToBounds = true
dismiss(animated:true, completion: nil) //5
}
thanks for any help rendered
Use a callback closure, it's less effort than delegate/protocol
In the popover view controller add a property
weak var callback : ((UIImage) -> ())?
Call it in didFinishPickingMediaWithInfo before dismiss
callback?(chosenImage)
In the parent view controller after creating the controller add
popoverViewController.callback = { image in
// do something with the image
}
popoverViewController is the popover view controller instance
you should create protocol
protocol PopOverImagePickerDelegate: NSObjectProtocol {
func didSelect(image: UIImage)
}
inside your PopoverViewController create
weak var delegate: PopOverImagePickerDelegate?
before navigating to PopoverViewController from ParentViewController set
popOverViewController.delegate = self
and then create
extension ParentViewController: PopOverImagePickerDelegate {
func didSelect(image: UIImage) {
//do stuff
}
}
and when image picked inside PopoverViewController just call
self.delegate?.didSelect(image: image)
I have 2 View Controller Scenes. Scene2 holds settings that will be used in scene1. Scene2 is accessed by menu item and its presented as show.
Now the issue is that the settings used in scene2 will not be used by scene1 unless the latter is refreshed.
I did try to trigger a viewDidLoad() of scene1 when the settings were saved at scene2 but it crashed.
class replaceSegue: NSStoryboardSegue {
override func perform() {
if let fromViewController = sourceController as? NSViewController {
if let toViewController = destinationController as? NSViewController {
// no animation.
fromViewController.view.window?.contentViewController = toViewController
}
}
}
}