SwiftUI tiny bits: Little view extension to log to the console

I wanted to log to the console in the middle of a SwiftUI chain but you can’t put “print” statements in SwiftUI code in most places (“Everything must be a View!” ;)). Since I was in a playground I couldn’t set a breakpoint that logged to the console (my standard strategy when in an application context). So I wrote this little thing:

extension View {
  func printMessage(_ msg: Any...,
              separator: String = " ",
              terminator: String = "\n")
            -> some View {
    // Print them out as if not
    // converted to an array.
    for m in msg {
      print(m,
            separator,
            terminator: "")
    }
    print()
    return self
  }
}

Use it like so:

import Cocoa
import PlaygroundSupport
import SwiftUI

extension View {
  func printMessage(_ msg: Any...,
        separator: String = " ",
        terminator: String = "\n")
      -> some View {
  // Print them out as if not
  // converted to an array.
  for m in msg {
    print(m,
      separator,
      terminator: "")
  }
  print()
  return self
  }
}

struct ContentView: View {
  var body: some View {
    VStack {
      Text("Hello")
      Text("World!")
        .padding()
        .background(
          GeometryReader { proxy in
            Color.clear
              .printMessage("Info: ", proxy.size)
        })
    }.padding()
  }
}


struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}

PlaygroundPage.current.liveView = NSHostingView(rootView: ContentView())

Output is:

Info:   (72.0, 48.0)
Info:   (72.0, 48.0)  
Info:   (72.0, 48.0)  

Could well be a better way or issues with this, but it’s just something I put together quickly to solve a problem (“what is the value of proxy.size?”) and it seems to work, so thought I’d share it. Improvements or better ways to get the same capabilities are welcome 🙂

Tiny Hints: a couple of Xcode 11.3 SwiftUI Preview tips

Xcode 11.3 & SwiftUI Previews: Dark Mode preview bug work-around, compact previews, multi-previews for ColorSchemes and Dynamic Text sizes.

Few small tips for previewing with SwiftUI that I just figured out.

Here’s the simple test ContentView.swift file to use in a standard single view iOS SwiftUI app template project in Xcode 11.3 that we’ll use to explore these tips:

import SwiftUI

struct ContentView: View {
  var body: some View {
    HStack {
      Image(systemName: "paperplane")
        .font(.largeTitle)
      VStack {
        Text("Title Text")
          .font(.title)
        Text("Subtitle goes here")
          .font(.subheadline)
      }
    }
  }
}

struct Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}

Standard Preview looks like this:

So, the first thing is WOW that takes a lot of space for just a small horizontal cell I’m working on (like for list row or whatever). It’s especially annoying if you’re trying to make sure it works well in dark mode, at different dynamic type sizes, etc. First tip is how to make that work better.

1 – How to preview a horizontal or smaller view in less space in the Xcode Canvas

Context: you’re working on a custom horizontal control, or a row in a list and you want to do multiple previews for light & dark mode, or different text size classes.

By default each preview gets an entire iPhone worth of chrome around it which means you have to scroll to check all your variations out – tiresome! Avoid this by using the .previewLayout modifier with a size that’s sufficient for your view. For example:

struct Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
      .previewLayout(.fixed(width: 400, height: 100))
  }
}

This produces a much more manageable preview like this:

The dimensions you use in .previewLayoutwill be specific to whatever you’re working on, of course, but this comes in really handy when you are previewing multiple versions of a horizontal view like this. Let’s demonstrate that with our next tip by making Dark Mode preview in Xcode 11.3 usable outside a NavigationView.

2 – Dark Mode Preview only seems to really work in NavigationViews

So the standard advice on how to preview in Dark Mode is to add .environment(.colorScheme, .dark) to the end of your view creation in PreviewProvider like so:

struct PreviewsDarkNotDark: PreviewProvider {
  static var previews: some View {
    ContentView()
      .previewLayout(.fixed(width: 400, height: 100))
      .border(Color.green)
      .environment(\.colorScheme, .dark)
  }
}

To make it clear what’s going on I’ve added a green border modifier to our ContentView so that we can see where it is when previewed like this because otherwise we’d wonder if it even exists!

Kind of useless :-/

Our views are using the colorScheme that was set and are rendering as if the device is in dark mode, but we can’t see it on a white background. So let’s fix that:

struct PreviewsActuallyDarkMode: PreviewProvider {
  static var previews: some View {
    ZStack {
      Color(.black)
      ContentView()
    }
    .previewLayout(.fixed(width: 400, height: 100))
    .environment(\.colorScheme, .dark)
  }
}

Which looks a lot better:

3 – Combining previews and reducing duplicate code

So now we have dark mode looking good so let’s combine the preceding two tips and look a both light and dark modes on the same screen and without having to scroll:

struct Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
      .previewLayout(.fixed(width: 400, height: 100))
  }
}

struct PreviewsActuallyDarkMode: PreviewProvider {
  static var previews: some View {
    ZStack {
      Color(.black)
      ContentView()
    }
    .previewLayout(.fixed(width: 400, height: 100))
    .environment(\.colorScheme, .dark)
  }
}

Which looks like this:

Looking pretty good! While writing them in separate PreviewProviders like that helpfully uses their struct names to label them, it does involve a bit of duplicated code. Here’s an alternate way to do it with a ViewModifier to put shared modifiers in and a Group of views which Xcode makes each their own Preview:

struct MyPreviewModifer: ViewModifier {
  var displayMode: ColorScheme = .light
  var name: String = ""
  
  func body(content: Content) -> some View {
    ZStack {
      Color(displayMode == .light ? .clear : .black)
      content
    }
    .previewLayout(.fixed(width: 400, height: 120))
    .previewDisplayName(name)
    .environment(\.colorScheme, displayMode)
  }
}


struct PreviewsLightDarkDynamicText: PreviewProvider {
  static var previews: some View {
    Group {
      ContentView()
        .modifier(MyPreviewModifer())
      
      ContentView()
        .modifier(MyPreviewModifer(displayMode: .dark))

      ContentView()
        .modifier(MyPreviewModifer(displayMode: .dark,
                        name: "dark extraSmall"))
        .environment(\.sizeCategory, .extraSmall)

      ContentView()
        .modifier(MyPreviewModifer(displayMode: .dark, 
                        name: "dark extraExtraLarge"))
        .environment(\.sizeCategory, .extraExtraLarge)
    }
  }
}

This gives us four previews and adds the ability to name them since they’re all in the same PreviewProvider struct and so don’t get named individually any longer. Looks like this now:

Well, this “tiny tips” got a little long… but hopefully you found these small tips to be useful.

(Code in gist here for easier copy and paste)

Snippet: SwiftUI View printMessage extension

Just a simple utility to print a string in the middle of a view chain when doing SwiftUI (pardon narrow formatting for blog):

extension View {
  func printMessage(_ msg: Any...,
              separator: String = " ",
              terminator: String = "\n")
            -> some View {
    // Print them out as if not
    // converted to an array.
    for m in msg {
      print(m,
            separator,
            terminator: "")
    }
    print()
    return self
  }
}

Use like this:

import SwiftUI
import PlaygroundSupport

struct ContentView: View {
  var body: some View {
    VStack {
      TestView(label: "hello", idx: 22)
        .font(.title)
      TestView(label: "hello", idx: 24)
        .foregroundColor(Color.red)
      Spacer()
    }
  }
}

struct TestView: View {
  let label: String
  let idx: Int

  var body: some View {
    Text(label)
      .padding(10)
      .printMessage("\(self.label):", "\(self.idx)")
     // or: .printMessage(self.label, self.idx)
  }
}

PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView())
// In a Swift playground the output is:

hello: 22
hello: 24
hello: 22
hello: 24
hello: 22
hello: 24

Gist here.

Tiny Hints: Swift Playground – landscape size for SwiftUI preview

Assuming you have a ContentView defined and want to see it in the Swift playground preview with a landscape aspect ratio, this code snippet seems to work:

let view = UIHostingController(rootView: ContentView())
// pick whatever size you want (this is iPhone 8 div 2):
view.preferredContentSize = CGSize(width: 667, height: 375)
PlaygroundPage.current.liveView = view

Might be a better way, but when I didn’t find a menu item in Xcode 11.3 I just coded this in to move forward. Seems to work to check layout, though it’s not strictly “landscape orientation” from a device standpoint ¯\_(ツ)_/¯: