Show dark mode and light mode side by side in Xcode - xcode

Is there a way to show dark mode and light mode side by side in Xcode 11?
I'm using UIKit / UIViewControllers.
(
Using SwiftUI and previews this can be done but does not apply to UIKit:
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
.environment(\.colorScheme, .light)
ContentView()
.environment(\.colorScheme, .dark)
}
}
}
#endif
)

When you use Storyboards, you can change the Interface Style of the preview.
When running, you can use the Environment Overrides to toggle between light and dark mode. But both option won't show the view side-by-side.
What you can probably do is to wrap your UIKit view in a helper SwiftUI View using UIViewRepresentable and display that in the preview canvas. That should work for embedded UIKit content.

Related

SwiftUI ScrollView - How to scroll up and down using arrow keys?

I have a SwiftUI view I wrap in a NSHostingController in a mac app. This view consists of a vertical ScrollView with LazyVStack in it with various content (divided into Section). This content is long so the view is always scrollable.
I'm trying to implement better keyboard navigation, I can navigate buttons using Tab key, but I also want to support basic scrolling using up and down arrow keys.
This is something that works out-of-the-box in AppKit when e.g. using NSTextView in NSScrollView.
How do I enable scrolling up and down using arrow keys for SwiftUI.ScrollView on macOS?
I'm to looking for scrolling to an identifiable element but rather to scroll by x points up and down. Like the NSScrollView works.
Ideally, including modifier keys ALT+Up to page up and CMD+Up to scroll to the top.
struct ContentView: View {
var body: some View {
ScrollView(.vertical) {
LazyVStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
Section("Lorem Ipsum") {
Text(Strings.longText)
}
}
.padding()
}
}
}

WKWebView shows white bar until window moved/resized

I'm trying to show a WKWebView in SwiftUI on MacOS. When the app initially loads, the WKWebView has a large, white bar at the top. Moving or resizing the window causes this to immediately disappear and display correctly. Interestingly, the blue border around the view does not exhibit the bad behavior.
My guess is that I'm missing some action in updateNSView.
I'm on MacOS 11.1 Big Sur, Xcode 12.2, Intel. Another thing to note is that I need to enable the "Outgoing Connections" entitlement in the App Sandbox to get WKWebView to render anything at all, even tho the content is provided locally from a string.
import SwiftUI
import WebKit
#main
struct ProblemWKWebViewApp: App {
var body: some Scene {
WindowGroup {
SwiftUIWebView()
.border(Color.blue, width: 2)
}
}
}
struct SwiftUIWebView: NSViewRepresentable {
public typealias NSViewType = WKWebView
func makeNSView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.loadHTMLString("<body style=\"background-color: red;\"><h1>Hello World!</h1></body>", baseURL: nil)
return webView
}
func updateNSView(_ nsView: WKWebView, context: Context) {
}
}
I had the same problem. I suppose that it is a bug. However I managed to fix it with adding ignoresSafeArea():
#main
struct ProblemWKWebViewApp: App {
var body: some Scene {
WindowGroup {
SwiftUIWebView()
.border(Color.blue, width: 2)
// Shows weird black bar on top without this on macOS
.ignoresSafeArea()
}
}
}

Preview SwiftUI code in Playground on different devices

XCode with can preview SwiftUI code on Canvas for many environments and devices with the help of PreviewProvider.
Like this:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
.environment((\.horizontalSizeClass), .compact)
ContentView()
.previewDevice(PreviewDevice(stringLiteral: "iPad8,5"))
.environment((\.horizontalSizeClass), .regular)
}
}
}
How can we do the same in Playground? The following .previewDevice() modifier has no effect in Playground (XCode 12.0.1).
PlaygroundPage.current.setLiveView(ContentView().previewDevice(PreviewDevice(stringLiteral: "iPhone 7")))
Maybe it can be done somehow with PlaygroundPage.current.liveView or else?

How do I get the window coordinates in SwiftUI?

I want to make a parallax background view, where the image behind the UI stays nearly still as the window moves around on the screen. To do this on macOS, I want to get the window's coordinates. How do I get the window's coordinates?
I ask this because I can't find anywhere that says how to do this:
Google searches which helped me find the following results:
SwiftUI window coordinates, SwiftUI window location, SwiftUI window get frame, SwiftUI get window, SwiftUI macOS window coordinates, SwiftUI macOS window location, SwiftUI macOS window get frame, SwiftUI macOS get window
Apple Developer Documentation:
GeometryReader - I had hoped that this would contain an API to give me the frame in system coordinate space, but it seems all the approaches it contains only reference within-the-window coordinates
Creating a macOS App — SwiftUI Tutorials - I was hoping Apple would have mentioned windowing in this, but it's not mentioned at all, aside from saying that you can preview the main window's contents in an Xcode preview pane
Fruitless searches: SwiftUI window coordinates, SwiftUI window location, SwiftUI window get frame
Other SO questions:
How to access own window within SwiftUI view? - I was optimistic that this would have an answer which would give me a SwiftUI API to access the window, but instead it uses a shim to access the AppKit window representation.
Define macOS window size using SwiftUI - Similar hopes as the above question, but this time the answer was just to read the frame of the content view, which again, always has a (0, 0) origin
SwiftUI coordinate space - I was hoping this answer would let me know how to transform the coordinates given by GeometryReader into screen coordinates, but unfortunately the coordinates are again constrained to within the window
Elsewhere on the web:
SwiftUI for Mac - Part 2 by TrozWare - I was hoping that this would give me some tips for using SwiftUI on Mac, such as interacting with windows, since most tutorials focus on iOS/iPadOS. Unfortunately, although it has lots of good information about how SwiftUI works with windows, it has no information on interacting with nor parsing those windows, themselves
SwiftUI Layout System by Alexander Grebenyuk - Was hoping for window layout within the screen, but this is all for full-screen iOS apps
SwiftUI by Example by Hacking with Swift - Was hoping for an example for how to get the position of a window, but it seems windows aren't really mentioned at all in the listed examples
As I listed, I found that all these either didn't relate to my issue, or only reference the coordinates within the window, but not the window's coordinates within the screen. Some mention ways to dip into AppKit, but I want to avoid that if possible.
The closest I got was trying to use a GeometryReader like this:
GeometryReader { geometry in
Text(verbatim: "\(geometry.frame(in: .global))")
}
but the origin was always (0, 0), though the size did change as I adjusted the window.
What I was envisioning was something perhaps like this:
public struct ParallaxBackground<Background: View>: View {
var background: Background
#Environment(\.windowFrame)
var windowFrame: CGRect
public var body: some View {
background
.offset(x: windowFrame.minX / 10,
y: windowFrame.minY / 10)
}
}
but \.windowFrame isn't real; it doesn't point to any keypath on EnvironmentValues. I can't find where I would get such a value.
As of today we have macOS 12 widely deployed/installed and SwiftUI has not gained a proper model for the macOS window. And from what I learned so far about macOS 13, there won't be a SwiftUI model for the window coming either.
Today (since macOS 11) we are not opening windows in the AppDelegate anymore but are now defining windows using the WindowGroup scene modifiers:
#main
struct HandleWindowApp: App {
var body: some Scene {
WindowGroup(id: "main") {
ContentView()
}
}
}
But there is no standard way to control or access the underlying window (e.g. NSWindow). To do this multiple answers on stackoverflow suggest to use a WindowAccessor which installs a NSView in the background of the ContentView and then accessing its window property. I also wrote my version of it to control the placement of windows.
In your case, it is sufficient to get a handle to the NSWindow instance and then observe the NSWindow.didMoveNotification. It will get called whenever the window did move.
If your app is using only a single window (e.g. you somehow inhibit that multiple windows can be created by the user), you can even observe the frames positions globally:
NotificationCenter.default
.addObserver(forName: NSWindow.didMoveNotification, object: nil, queue: nil) { (notification) in
if let window = notification.object as? NSWindow,
type(of: window).description() == "SwiftUI.SwiftUIWindow"
{
print(window.frame)
}
}
If you want the window frame:
The SceneDelegate keeps track of all the windows, so you can use it to make an EnvironmentObject with a reference to their frames and pass that to your View. Update the environment object values in the delegate method: func windowScene(_ windowScene: UIWindowScene, didUpdate previousCoordinateSpace: UICoordinateSpace, ...
If it's a one window app, it's much more straight forward. You could use UIScreen.main.bounds (if full screen) or a computed variable in you view:
var frame: CGRect { (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window?.frame ?? .zero }
But if you are looking for the frame of the view in the window, try something like this:
struct ContentView: View {
#State var frame: CGRect = .zero
var orientationChangedPublisher = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)
var body: some View {
VStack {
Text("text frame georeader \(frame.debugDescription)")
}
.background(GeometryReader { geometry in
Color.clear // .edgesIgnoringSafeArea(.all) // may need depending
.onReceive(self.orientationChangedPublisher.removeDuplicates()) { _ in
self.frame = geometry.frame(in: .global)
}
})
}
}
But having said all that, usually you don't need an absolute frame. Alignment guides let you place things relative to each other.
// For macOS App, using Frame Changed Notification and passing as Environment Object to SwiftUI View
class WindowInfo: ObservableObject {
#Published var frame: CGRect = .zero
}
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
let windowInfo = WindowInfo()
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
.environmentObject(windowInfo)
// Create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.contentView?.postsFrameChangedNotifications = true
window.makeKeyAndOrderFront(nil)
NotificationCenter.default.addObserver(forName: NSView.frameDidChangeNotification, object: nil, queue: nil) { (notification) in
self.windowInfo.frame = self.window.frame
}
}
struct ContentView: View {
#EnvironmentObject var windowInfo: WindowInfo
var body: some View {
Group {
Text("Hello, World! \(windowInfo.frame.debugDescription)")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}

Is there any way to use storyboard and SwiftUI in same iOS Xcode project?

As Swift 5 introduces the SwiftUI framework for creating the views, but we are currently using the storyboard for UI design.
So I just wanted to know the procedure to use Storyboard and Swift UI in same iOS Single View Application.
I just started to look at the SwiftUI. Sharing a small example.
In the storyboard add Hosting View Controller
Subclass the UIHostingController with your own class (ChildHostingController)
ChildHostingController should look something like that:
import UIKit
import SwiftUI
struct SecondView: View {
var body: some View {
VStack {
Text("Second View").font(.system(size: 36))
Text("Loaded by SecondView").font(.system(size: 14))
}
}
}
class ChildHostingController: UIHostingController<SecondView> {
required init?(coder: NSCoder) {
super.init(coder: coder,rootView: SecondView());
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
For more details have a look at Custom UIHostingController
Apple Docs UIhostingController (Unfortunatelly it hasn't been documented yet)
Integrating SwiftUI Video
Mixing UIKit with SwiftUI
Both can be embedded and mixed SwiftUI in Storyboards and the other way around
UIViewControllerRepresentable used for embedding Storyboards in SwiftUI
Create the ViewController on a Storyboard and give it a Storyboard ID
import SwiftUI
struct ContentView: View {
var body: some View {
StoryboardViewController()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct StoryboardViewController: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> some UIViewController {
let storyboard = UIStoryboard(name: "Storyboard", bundle: Bundle.main)
let controller = storyboard.instantiateViewController(identifier: "Main")
return controller
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
Communication between UIKit and SwiftUI can be through #Binding variable. This will let SwiftUI view inject variable state and control the it, func updateUIViewController will be called to do the work when changed.
UIViewRepresentable used same way for views
import SwiftUI
struct ContentView: View {
#State var color = UIColor.green
var body: some View {
SampleView(color: $color).frame(width: 100, height: 100
, alignment: .center)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct SampleView: UIViewRepresentable {
#Binding var color: UIColor
func makeUIView(context: Context) -> some UIView {
return UIView()
}
func updateUIView(_ uiView: UIViewType, context: Context) {
uiView.backgroundColor = color
}
}
UIHostingController used for embedding SwiftUI in UIKit
First is to be created programmatically
let childViewController = UIHostingController(rootView: SwiftUIContentView())
addChild(childViewController)
childViewController.view.frame = frame
view.addSubview(childViewController.view)
childViewController.didMove(toParent: self)
Second with Xcode 11 by adding Hosting View Controller to the storyboard and create a segue to it and then create an #IBSegueAction by Control-drag from the segue to your ViewController and then create an instance of the SwiftUI view and pass it to HostingViewController initializer
Third is by adding Hosting View Controller to the storyboard and then subclassing it as mentioned in the previous answer above https://stackoverflow.com/a/58250271/3033056
Yes you can do that! Here are the steps you can take to do so:
Go to your current Xcode project -> Storyboard, click on the + sign (right upper corner) and search for Hosting Controller (just like you would for a button or label).
Drag Hosting Controller to your Storyboard. Create a Segue connection from your UI element (I'm using a button) to that Hosting Controller and select Push.
Create an outlet connection from that Segue to your View Controller (it's a new feature - just like you would create an outlet for a Label), and name it.
Declare your view inside of this outlet connection (you can do that, don't have to use PrepareForSegue method), and return it.
For example: I created a SwiftUI view in my current project (in Xcode: File -> New -> File -> SwiftUI View) and called it DetailsView. My outlet connection would look like this:
import UIKit
import SwiftUI
class ViewController: UIViewController {
#IBSegueAction func showDetails(_ coder: NSCoder) -> UIViewController? {
let detailsView = DetailsView()
return UIHostingController(coder: coder, rootView: detailsView)
}
override func viewDidLoad() {
super.viewDidLoad()
// some code
}
}
That's it! Now run it.
as Edgell Mentioned, there is a new ViewController named HostViewController that can host a SwiftUI page inside it.
there's a complete talk about integrating SwiftUI in existing project at WWDC that answers your question very well.
Integrating SwiftUI:
https://developer.apple.com/videos/play/wwdc2019/231/
WWDC19:
https://developer.apple.com/videos/
Add SwiftUI to a UIKit project:
1) Display a complete screen from SwiftUI:
If this is your case, you need just to follow this article or check one of the above answers.
2) Add SwiftUI view inside UIViewController:
In this case, follow the following steps:
Add this extension to your project:
This extension is copy pasted from a scale project in production:
import UIKit
import SwiftUI
extension UIViewController {
/// component: View created by SwiftUI
/// targetView: The UIView that will host the component
func host(component: AnyView, into targetView: UIView) {
let controller = UIHostingController(rootView: component)
self.addChild(controller)
controller.view.translatesAutoresizingMaskIntoConstraints = false
targetView.addSubview(controller.view)
controller.didMove(toParent: self)
NSLayoutConstraint.activate([
controller.view.widthAnchor.constraint(equalTo: targetView.widthAnchor, multiplier: 1),
controller.view.heightAnchor.constraint(equalTo: targetView.heightAnchor, multiplier: 1),
controller.view.centerXAnchor.constraint(equalTo: targetView.centerXAnchor),
controller.view.centerYAnchor.constraint(equalTo: targetView.centerYAnchor)
])
}
}
Use the extension inside your viewDidLoad:
import UIKit
import SwiftUI
class ViewController: UIViewController {
// MARK: - Outlets
// This view should have the needed constraints
// bc we are going to magically convert it to your `SwiftUI` view!
#IBOutlet weak var myUIView: UIView!
// MARK: - Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
// Create instance from your `SwiftUI` view
let mySwiftUIView = SomeAmazingView()
// Use the extension
self.host(component: AnyView(mySwiftUIView), into: myUIView)
}
}
That's all, good luck!
You need to add a "Host View Controller" to the Storyboard. The SwiftUI form will be displayed in the Host View Controller and is callable from any Storyboard Form.
Be advised, the Host View Controller does not display in the Library for Xcode 11 on Mohave, you must be on Catalina. This is a bug, because once you have a project with a Host View Controller created on Catalina, that same project will run fine on Mohave, in fact, you can even copy that Host View Controller to other Storyboards and it will wok fine.

Resources