How to Apply card view cornerRadius & shadow like iOS appstore in swift 4 - xcode

I want to apply "cornerRadius" and card view "shadow" in my collection view cell like iOS appstore today View.

Just add a subview to the cell and manipulate it's layer property. Tweak the values to your liking. The following code should give a similar result to how it looks in the App Store:
// The subview inside the collection view cell
myView.layer.cornerRadius = 20.0
myView.layer.shadowColor = UIColor.gray.cgColor
myView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
myView.layer.shadowRadius = 12.0
myView.layer.shadowOpacity = 0.7

Create a new UIView subclass named "CardView" like below:
import Foundation
import UIKit
#IBDesignable
class CardView: UIView {
#IBInspectable var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
layer.shadowRadius = newValue
layer.masksToBounds = false
}
}
#IBInspectable var shadowOpacity: Float {
get {
return layer.shadowOpacity
}
set {
layer.shadowOpacity = newValue
layer.shadowColor = UIColor.darkGray.cgColor
}
}
#IBInspectable var shadowOffset: CGSize {
get {
return layer.shadowOffset
}
set {
layer.shadowOffset = newValue
layer.shadowColor = UIColor.black.cgColor
layer.masksToBounds = false
}
}
}
Then just set "CardView" as Custom Class for your view from XCode Interface Builder. It's simple and easily configurable!

- SwiftUI
struct SimpleRedView: View {
var body: some View {
Rectangle()
.foregroundColor(.red)
.frame(width: 340, height: 500, alignment: .center)
}
}
struct ContentView: View {
var body: some View {
SimpleRedView()
.cornerRadius(28)
.shadow(radius: 16, y: 16)
}
}
SimpleRedView() is just for placeholder and you can replace it with any kind of View you like.

To add onto #oyvindhauge 's great answer -- make sure none of the subviews inside myView extend to the edges. For instance, my card view contained a table view that fills the card -- so it's necessary to set the tableView.layer.cornerRadius = 20.0 as well. This applies to any subview that fills the card.

Related

Fading Text in with Duration in SwiftUI

I have been using the following UIView extension for quite a while to fade text in and out. I have been trying to figure out how to implement this with SwiftUI but so far haven't found anything that exactly addresses this for me. Any help would be greatly appreciated.
extension UIView {
func fadeKey() {
// Move our fade out code from earlier
UIView.animate(withDuration: 3.0, delay: 2.0, options: UIView.AnimationOptions.curveEaseIn, animations: {
self.alpha = 1.0 // Instead of a specific instance of, say, birdTypeLabel, we simply set [thisInstance] (ie, self)'s alpha
}, completion: nil)
}
func fadeIn1() {
// Move our fade out code from earlier
UIView.animate(withDuration: 1.5, delay: 0.5, options: UIView.AnimationOptions.curveEaseIn, animations: {
self.alpha = 1.0 // Instead of a specific instance of, say, birdTypeLabel, we simply set [thisInstance] (ie, self)'s alpha
}, completion: nil)
}
I assume this is what you wanted. Try this below code:
struct FadeView: View {
#State var isClicked = false
#State var text = "Faded Text"
var body: some View {
VStack {
Text(text) //fade in and fade out
.opacity(isClicked ? 0 : 1)
Button("Click to animate") {
withAnimation {
isClicked.toggle()
}
}
}
}
}
You could use withAnimation function and manipulate the duration, options and delay
struct ContentView: View {
#State var isActive = false
var body: some View {
VStack {
Button("Fade") {
withAnimation(.easeIn(duration: 3.0).delay(2)){
isActive.toggle()
}
}
Rectangle()
.frame(width: 222.0, height: 222.0)
.opacity(isActive ? 0 : 1)
}
}
}

How to trigger SwiftUI animation via change of non-State variable

It's easy to have an animation begin when a view appears, by using .onAppear(). But I'd like to perform a repeating animation whenever one of the view's non-State variables changes. An example:
Here is a view that I would like to "throb" whenever its throbbing parameter, set externally, is true:
struct MyCircle: View {
var throbbing: Bool
#State var scale = 1.0
var body: some View {
Circle()
.frame(width: 100 * scale, height: 100 * scale)
.foregroundColor(.blue)
.animation(.easeInOut.repeatForever(), value: scale)
.onAppear { scale = 1.2 }
}
}
Currently the code begins throbbing immediately, regardless of the throbbing variable.
But imagine this scenario:
struct ContentView: View {
#State var throb: Bool = false
var body: some View {
VStack {
Button("Throb: \(throb ? "ON" : "OFF")") { throb.toggle() }
MyCircle(throbbing: throb)
}
}
}
It looks like this:
Any ideas how I can modify MyCircle so that the throbbing starts when the button is tapped and ends when it is tapped again?
You can use onChange to watch throbbing and then assign an animation. If true, add a repeating animation, and if false, just animate back to the original scale size:
struct MyCircle: View {
var throbbing: Bool
#State private var scale : CGFloat = 0
var body: some View {
Circle()
.frame(width: 100, height: 100)
.scaleEffect(1 + scale)
.foregroundColor(.blue)
.onChange(of: throbbing) { newValue in
if newValue {
withAnimation(.easeInOut.repeatForever()) {
scale = 0.2
}
} else {
withAnimation {
scale = 0
}
}
}
}
}
There're two interesting things that i've just found when trying to achieve yours target.
Animation will be added when the value changed
Animation that added to the view cannot be removed, and we need to drop the animated view from the view hiearachy by remove its id, new view will be created with zero animations.
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
#State var throbbling = false
#State var circleId = UUID()
var body: some View {
VStack {
Toggle("Throbbling", isOn: $throbbling)
circle.id(circleId)
.scaleEffect(throbbling ? 1.2 : 1.0)
.animation(.easeInOut.repeatForever(), value: throbbling)
.onChange(of: throbbling) { newValue in
if newValue == false {
circleId = UUID()
}
}
}.padding()
}
#ViewBuilder
var circle: some View {
Circle().fill(.green).frame(width: 100, height: 100)
}
}
PlaygroundPage.current.setLiveView(ContentView())

How do I add a toolbar to a macOS app using SwiftUI?

I am trying to add a toolbar inside the title bar to a macOS app using SwiftUI, something similar to what is shown below.
I am unable to figure out a way to achieve this using SwiftUI. Currently, I have my toolbar (which just has a text field) inside my view, but I want to move it into the title bar.
My current code:
struct TestView: View {
var body: some View {
VStack {
TextField("Placeholder", text: .constant("")).padding()
Spacer()
}
}
}
So, in my case, I need to have the textfield inside the toolbar.
As of macOS 11 you’ll likely want to use the new API as documented in WWDC Session 10104 as the new standard. Explicit code examples were provided in WWDC Session 10041 at the 12min mark.
NSWindowToolbarStyle.unified
or
NSWindowToolbarStyle.unifiedCompact
And in SwiftUI you can use the new .toolbar { } builder.
struct ContentView: View {
var body: some View {
List {
Text("Book List")
}
.toolbar {
Button(action: recordProgress) {
Label("Record Progress", systemImage: "book.circle")
}
}
}
private func recordProgress() {}
}
Approach 1:
This is done by adding a titlebar accessory. I was able to get this done by modifying the AppDelegate.swift file. I had to apply some weird padding to make it look right.
AppDelegate.swift
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Create the titlebar accessory
let titlebarAccessoryView = TitlebarAccessory().padding([.top, .leading, .trailing], 16.0).padding(.bottom,-8.0).edgesIgnoringSafeArea(.top)
let accessoryHostingView = NSHostingView(rootView:titlebarAccessoryView)
accessoryHostingView.frame.size = accessoryHostingView.fittingSize
let titlebarAccessory = NSTitlebarAccessoryViewController()
titlebarAccessory.view = accessoryHostingView
// 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")
// Add the titlebar accessory
window.addTitlebarAccessoryViewController(titlebarAccessory)
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
TitlebarAccessory.swift
import SwiftUI
struct TitlebarAccessory: View {
var body: some View {
TextField("Placeholder", text: .constant(""))
}
}
Result:
Approach 2 (Alternative method):
The idea here is to do the toolbar part using storyboard and the rest of the app using SwiftUI. This is done by creating a new app with storyboard as the user interface. Then go to the storyboard and delete the default View Controller and add a new NSHostingController. Connect the newly added Hosting Controller to the main window by setting its relationship. Add your toolbar to the window using interface builder.
Attach a custom class to your NSHostingController and load your SwiftUI view into it.
Example code below:
import Cocoa
import SwiftUI
class HostingController: NSHostingController<SwiftUIView> {
#objc required dynamic init?(coder: NSCoder) {
super.init(coder: coder, rootView: SwiftUIView())
}
}
Using this approach also gives you the ability to customize the toolbar.
Inspired by your first approach I managed to get a toolbar too. As I'm using Divider()s in it, your Paddings didn't work great for me.
This one seems to work a bit smoother with different Layout-Sizes:
let titlebarAccessoryView = TitlebarAccessory().padding([.leading, .trailing], 10).edgesIgnoringSafeArea(.top)
let accessoryHostingView = NSHostingView(rootView:titlebarAccessoryView)
accessoryHostingView.frame.size.height = accessoryHostingView.fittingSize.height+16
accessoryHostingView.frame.size.width = accessoryHostingView.fittingSize.width
Maybe there is an even smoother way to get rid of this +16 and the padding trailing and leading (there are several other options instead of fittingSize), but I couldn't find any that looks great without adding numerical values.
I've finally managed to do this without any fiddly padding and in a way which looks great in full screen as well. Also, The previous solutions do not allow horizontal resizing.
Wrap your title view in an HStack() and add in an invisible text view which is allowed to expand to infinity height. This seems to be what keeps everything centered. Ignore the safe area at the top to now center it in the full height of the titlebar.
struct TitleView : View {
var body: some View {
HStack {
Text("").font(.system(size: 0, weight: .light, design: .default)).frame(maxHeight: .infinity)
Text("This is my Title")
}.edgesIgnoringSafeArea(.top)
}
}
In your app delegate, when you add in the NSTitlebarAccessoryViewController() set the layoutAttribute to top. This will allow it to resize horizontally as your window size changes (leading and left fix the width to minimums and has caused me a lot of pain looking for the answer to this.
let titlebarAccessoryView = TitleView()
let accessoryHostingView = NSHostingView(rootView: titlebarAccessoryView)
accessoryHostingView.frame.size = accessoryHostingView.fittingSize
let titlebarAccessory = NSTitlebarAccessoryViewController()
titlebarAccessory.view = accessoryHostingView
titlebarAccessory.layoutAttribute = .top
In my case I also want some buttons on the very right which position independently of the rest of the title, so I chose to add them separately, making use of the ability to add multiple view controllers
let titlebarAccessoryRight = NSTitlebarAccessoryViewController()
titlebarAccessoryRight.view = accessoryHostingRightView
titlebarAccessoryRight.layoutAttribute = .trailing
window.toolbar = NSToolbar()
window.toolbar?.displayMode = .iconOnly
window.addTitlebarAccessoryViewController(titlebarAccessory)
window.addTitlebarAccessoryViewController(titlebarAccessoryRight)
https://developer.apple.com/documentation/uikit/uititlebar
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
if let titlebar = windowScene.titlebar {
//toolbar
let identifier = NSToolbar.Identifier(toolbarIdentifier)
let toolbar = NSToolbar(identifier: identifier)
toolbar.allowsUserCustomization = true
toolbar.centeredItemIdentifier = NSToolbarItem.Identifier(rawValue: centerToolbarIdentifier)
titlebar.toolbar = toolbar
titlebar.toolbar?.delegate = self
titlebar.titleVisibility = .hidden
titlebar.autoHidesToolbarInFullScreen = true
}
window.makeKeyAndVisible()
}
#if targetEnvironment(macCatalyst)
let toolbarIdentifier = "com.example.apple-samplecode.toolbar"
let centerToolbarIdentifier = "com.example.apple-samplecode.centerToolbar"
let addToolbarIdentifier = "com.example.apple-samplecode.add"
extension SceneDelegate: NSToolbarDelegate {
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
if itemIdentifier == NSToolbarItem.Identifier(rawValue: toolbarIdentifier) {
let group = NSToolbarItemGroup(itemIdentifier: NSToolbarItem.Identifier(rawValue: toolbarIdentifier), titles: ["Solver", "Resistance", "Settings"], selectionMode: .selectOne, labels: ["section1", "section2", "section3"], target: self, action: #selector(toolbarGroupSelectionChanged))
group.setSelected(true, at: 0)
return group
}
if itemIdentifier == NSToolbarItem.Identifier(rawValue: centerToolbarIdentifier) {
let group = NSToolbarItemGroup(itemIdentifier: NSToolbarItem.Identifier(rawValue: centerToolbarIdentifier), titles: ["Solver1", "Resistance1", "Settings1"], selectionMode: .selectOne, labels: ["section1", "section2", "section3"], target: self, action: #selector(toolbarGroupSelectionChanged))
group.setSelected(true, at: 0)
return group
}
if itemIdentifier == NSToolbarItem.Identifier(rawValue: addToolbarIdentifier) {
let barButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.add, target: self, action: #selector(self.add(sender:)))
let button = NSToolbarItem(itemIdentifier: itemIdentifier, barButtonItem: barButtonItem)
return button
}
return nil
}
#objc func toolbarGroupSelectionChanged(sender: NSToolbarItemGroup) {
print("selection changed to index: \(sender.selectedIndex)")
}
#objc func add(sender: UIBarButtonItem) {
print("add clicked")
}
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
[NSToolbarItem.Identifier(rawValue: toolbarIdentifier), NSToolbarItem.Identifier(rawValue: centerToolbarIdentifier), NSToolbarItem.Identifier.flexibleSpace,
NSToolbarItem.Identifier(rawValue: addToolbarIdentifier),
NSToolbarItem.Identifier(rawValue: addToolbarIdentifier)]
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
self.toolbarDefaultItemIdentifiers(toolbar)
}
}
#endif

Reset offset, onTapGesture works, but onRotated does not work, why?

I want the view containing 2 rectangles floating in the bottom of the screen, whatever the orientation is portrait or landscape.
code is a test, when orientationDidChangeNotification happened, I Found UIDevice.current.orientation.isPortrait and UIScreen.main.bounds.height often have wrong value, why?
Anyway, test code is just reset offset = 0 in onRotated(). but it doesn't work; otherwise onTapGesture works fine.
Q1: Is it a wrong way for SwiftUI? SceneDelegate.orientationDidChangeNotification -> contentView.onRotated()?
Q2: why do UIDevice.current.orientation.isPortrait and UIScreen.main.bounds.height often have wrong value?
Q3: How to let a view float at the bottom of screen in both portrait and landscape?
let height: CGFloat = 100
struct TestView: View {
#State var offset = (UIScreen.main.bounds.height - height) / 2
var body: some View {
ZStack {
Text("+")
VStack(spacing: 0) {
Rectangle().fill(Color.blue)
Rectangle().fill(Color.red)
}
.frame(width: 100, height: height)
.offset(y: offset)
.onTapGesture {
self.offset = 0
}
}
}
func onRotated() {
// let isPortrait = UIDevice.current.orientation.isPortrait
offset = 0//(UIScreen.main.bounds.height - height) / 2
// print("\(isPortrait), screen height = \(UIScreen.main.bounds.height)")
}
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = TestView()
if let windowScene = scene as? UIWindowScene {
NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification, object: nil, queue: nil) { notification in
contentView.onRotated()
}
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
...
}
contentView is not a reference, it is a value, so you call .onRotated on own copy of contentView value that lives only within callback
let contentView = TestView()
if let windowScene = scene as? UIWindowScene {
NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification, object: nil, queue: nil) { notification in
contentView.onRotated() // local copy!!!
}
instead create listener for notification publisher inside TestView, so it can change self internally.
Moreover, it is not clear the intention but SwiftUI gives possibility to track size classes via EnvironmentValues.horizontalSizeClass and EnvironmentValues.verticalSizeClass which are automatically changed on device orientation, so it is possible to make your view layout depending on those environment values even w/o notification.
See here good example on how to use size classes

How do I render a SwiftUI View that is not at the root hierarchy as a UIImage?

Suppose I have a simple SwiftUI View that is not the ContentView such as this:
struct Test: View {
var body: some View {
VStack {
Text("Test 1")
Text("Test 2")
}
}
}
How can I render this view as a UIImage?
I've looked into solutions such as :
extension UIView {
func asImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}
}
But it seems that solutions like that only work on UIView, not a SwiftUI View.
Here is the approach that works for me, as I needed to get image exactly sized as it is when placed alongside others. Hope it would be helpful for some else.
Demo: above divider is SwiftUI rendered, below is image (in border to show size)
Update: re-tested with Xcode 13.4 / iOS 15.5
Test module in project is here
extension View {
func asImage() -> UIImage {
let controller = UIHostingController(rootView: self)
// locate far out of screen
controller.view.frame = CGRect(x: 0, y: CGFloat(Int.max), width: 1, height: 1)
let size = controller.sizeThatFits(in: UIScreen.main.bounds.size)
controller.view.bounds = CGRect(origin: .zero, size: size)
controller.view.sizeToFit()
UIApplication.shared.windows.first?.rootViewController?.view.addSubview(controller.view)
let image = controller.view.asImage()
controller.view.removeFromSuperview()
return image
}
}
extension UIView {
func asImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
// [!!] Uncomment to clip resulting image
// rendererContext.cgContext.addPath(
// UIBezierPath(roundedRect: bounds, cornerRadius: 20).cgPath)
// rendererContext.cgContext.clip()
// As commented by #MaxIsom below in some cases might be needed
// to make this asynchronously, so uncomment below DispatchQueue
// if you'd same met crash
// DispatchQueue.main.async {
layer.render(in: rendererContext.cgContext)
// }
}
}
}
// TESTING
struct TestableView: View {
var body: some View {
VStack {
Text("Test 1")
Text("Test 2")
}
}
}
struct TestBackgroundRendering: View {
var body: some View {
VStack {
TestableView()
Divider()
Image(uiImage: render())
.border(Color.black)
}
}
private func render() -> UIImage {
TestableView().asImage()
}
}
Solution of Asperi works, but if you need image without white background you have to add this line:
controller.view.backgroundColor = .clear
And your View extension will be:
extension View {
func asImage() -> UIImage {
let controller = UIHostingController(rootView: self)
// locate far out of screen
controller.view.frame = CGRect(x: 0, y: CGFloat(Int.max), width: 1, height: 1)
UIApplication.shared.windows.first!.rootViewController?.view.addSubview(controller.view)
let size = controller.sizeThatFits(in: UIScreen.main.bounds.size)
controller.view.bounds = CGRect(origin: .zero, size: size)
controller.view.sizeToFit()
controller.view.backgroundColor = .clear
let image = controller.view.asImage()
controller.view.removeFromSuperview()
return image
}
}

Resources