Related
Currently, I have this:
I want the width of my red ContentView not 100%, I want a leading and trailing space.
I tried to set leading and trailing constants of my red ContentView which resulted in horizontal scrolling. I tried to disable horizontal scrolling, which resulted in a fixed ContentView with leading space to my UIScrollView.
How can I define values for trailing and leading constants on my ContentView, without the unwanted behaviour (horizontal scrolling)?
You want to take advantage of the scroll view's Content Layout Guide and Frame Layout Guide.
Constrain your "ContentView" to the Content Layout Guide to control the "scrollable size" and constrain it to the Frame Layout Guide to control its size.
So, for example, if you want 20-points of "padding" on left and right, constrain its Leading to the Content Layout Guide Leading with a Constant of 20, and constraint its Width to the Frame Layout Guide with a constant of minus 40 (20 points on each side).
Here's how it can look if you're doing this in Storyboard:
change the Height constraint on the ContentView from 120 to 1200 to see the vertical scrolling.
Here's the source for that storyboard so you can inspect it:
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Y6W-OH-hqX">
<device id="retina3_5" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="s0d-6b-0kx">
<objects>
<viewController id="Y6W-OH-hqX" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
<rect key="frame" x="0.0" y="0.0" width="320" height="480"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="gbkg" translatesAutoresizingMaskIntoConstraints="NO" id="kGy-CW-5oZ">
<rect key="frame" x="20" y="20" width="280" height="440"/>
</imageView>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="f6w-Hj-SnQ">
<rect key="frame" x="20" y="20" width="280" height="440"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bKG-7C-UV8" userLabel="ContentView">
<rect key="frame" x="20" y="20" width="240" height="120"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Your Content View" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5vI-Ro-IQh">
<rect key="frame" x="8" y="8" width="224" height="24"/>
<fontDescription key="fontDescription" type="system" pointSize="20"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" systemColor="systemRedColor"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="5vI-Ro-IQh" secondAttribute="trailing" constant="8" id="SOR-6B-qUy"/>
<constraint firstItem="5vI-Ro-IQh" firstAttribute="leading" secondItem="bKG-7C-UV8" secondAttribute="leading" constant="8" id="Tdy-3P-f8D"/>
<constraint firstItem="5vI-Ro-IQh" firstAttribute="top" secondItem="bKG-7C-UV8" secondAttribute="top" constant="8" id="V5A-6U-nMX"/>
<constraint firstAttribute="height" constant="120" id="egx-Ye-Pn6"/>
</constraints>
</view>
</subviews>
<constraints>
<constraint firstItem="bKG-7C-UV8" firstAttribute="leading" secondItem="ds4-RC-JaM" secondAttribute="leading" constant="20" id="0Gd-qA-Ieg"/>
<constraint firstItem="bKG-7C-UV8" firstAttribute="trailing" secondItem="ds4-RC-JaM" secondAttribute="trailing" id="WGf-cI-cwg"/>
<constraint firstItem="bKG-7C-UV8" firstAttribute="bottom" secondItem="ds4-RC-JaM" secondAttribute="bottom" constant="-20" id="hkb-0t-zAy"/>
<constraint firstItem="bKG-7C-UV8" firstAttribute="width" secondItem="pcX-Se-Do0" secondAttribute="width" constant="-40" id="ndP-Ww-TIg"/>
<constraint firstItem="bKG-7C-UV8" firstAttribute="top" secondItem="ds4-RC-JaM" secondAttribute="top" constant="20" id="xbu-qJ-MLf"/>
</constraints>
<viewLayoutGuide key="contentLayoutGuide" id="ds4-RC-JaM"/>
<viewLayoutGuide key="frameLayoutGuide" id="pcX-Se-Do0"/>
</scrollView>
</subviews>
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
<color key="backgroundColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="f6w-Hj-SnQ" firstAttribute="bottom" secondItem="kGy-CW-5oZ" secondAttribute="bottom" id="INT-yJ-xuH"/>
<constraint firstItem="kGy-CW-5oZ" firstAttribute="top" secondItem="vDu-zF-Fre" secondAttribute="top" constant="20" id="QF2-gZ-vye"/>
<constraint firstItem="f6w-Hj-SnQ" firstAttribute="top" secondItem="kGy-CW-5oZ" secondAttribute="top" id="S8N-mU-Qc2"/>
<constraint firstItem="f6w-Hj-SnQ" firstAttribute="trailing" secondItem="kGy-CW-5oZ" secondAttribute="trailing" id="Uv9-tZ-0tY"/>
<constraint firstItem="kGy-CW-5oZ" firstAttribute="leading" secondItem="vDu-zF-Fre" secondAttribute="leading" constant="20" id="YQh-qf-dC6"/>
<constraint firstItem="vDu-zF-Fre" firstAttribute="bottom" secondItem="kGy-CW-5oZ" secondAttribute="bottom" constant="20" id="ZCH-4J-Gvo"/>
<constraint firstItem="vDu-zF-Fre" firstAttribute="trailing" secondItem="kGy-CW-5oZ" secondAttribute="trailing" constant="20" id="eAS-mf-ueb"/>
<constraint firstItem="f6w-Hj-SnQ" firstAttribute="leading" secondItem="kGy-CW-5oZ" secondAttribute="leading" id="gvC-t3-aUK"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="13.125" y="138.75"/>
</scene>
</scenes>
<resources>
<image name="gbkg" width="667" height="1200"/>
<systemColor name="systemRedColor">
<color red="1" green="0.23137254901960785" blue="0.18823529411764706" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>
And here's how you could do it via code:
class ViewController: UIViewController {
let contentView = UIView()
let contentLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
guard let bkgImage = UIImage(named: "gbkg") else { return }
view.backgroundColor = .darkGray
// background image view
let bkgImageView = UIImageView()
bkgImageView.image = bkgImage
// create the scroll view
let scrollView = UIScrollView()
// label preoperties
contentLabel.textAlignment = .center
contentLabel.font = .systemFont(ofSize: 24.0)
contentLabel.textColor = .white
contentLabel.numberOfLines = 0
contentLabel.text = "Your Content View"
// add label to contentView
contentView.addSubview(contentLabel)
// content view properties
contentView.backgroundColor = .systemRed
contentView.layer.cornerRadius = 24
// add content view to scroll view
scrollView.addSubview(contentView)
// we'll use auto-layout for all views
[bkgImageView, scrollView, contentView, contentLabel].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
// add views to main view
view.addSubview(bkgImageView)
view.addSubview(scrollView)
// so we can do less typing in constraints
let g = view.safeAreaLayoutGuide
let cg = scrollView.contentLayoutGuide
let fg = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// inset background image view 20-points on all 4 sides
bkgImageView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
bkgImageView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
bkgImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
bkgImageView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
// constrain scroll view to background image view
scrollView.topAnchor.constraint(equalTo: bkgImageView.topAnchor, constant: 0.0),
scrollView.leadingAnchor.constraint(equalTo: bkgImageView.leadingAnchor, constant: 0.0),
scrollView.trailingAnchor.constraint(equalTo: bkgImageView.trailingAnchor, constant: 0.0),
scrollView.bottomAnchor.constraint(equalTo: bkgImageView.bottomAnchor, constant: 0.0),
// constrain content view to scroll view's Content Layout Guide
// top/leading/bottom with 20-points "padding"
contentView.topAnchor.constraint(equalTo: cg.topAnchor, constant: 20.0),
contentView.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 20.0),
contentView.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: -20.0),
// we can leave trailing at 0.0
contentView.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: 0.0),
// constrain content view width to scroll view's Frame Layout Guide
// minus 40-points (20 on each side)
contentView.widthAnchor.constraint(equalTo: fg.widthAnchor, constant: -40.0),
// constrain label near top of content view
contentLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8.0),
contentLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8.0),
contentLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8.0),
// content view height will probably be determined by your UI elements,
// but for this example we'll use an explicit height of 120
contentView.heightAnchor.constraint(equalToConstant: 120.0),
// to see the vertical scrolling, use this height instead
//contentView.heightAnchor.constraint(equalToConstant: 1200.0),
])
}
}
Try to set up a width constraint for your scroll view. For example, if you want to have its width the same as the root view's width:
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
This way, the layout system knows that the scroll view should not grow horizontally.
I have a UICollectionView placed inside a UITableViewCell. The collection view has its scroll direction set to horizontal. However, when I swipe left or right on it to try and scroll, it doesn't work. Instead the table view cell is just pressed. It seems like the table view is eating up the gesture and not allowing the collection view to register it.
How can I make it so the collection view can scroll left and right with horizontal swipes, while still allowing the parent table view to scroll vertically?
I went through this SO question and its accepted answer's linked blog post, but it did not seem to address the issue I am having. On a whim, I also tried throwing sendSubviewToBack(self.contentView) in my table view cell's awakeFromNib() based on this question, but it didn't seem to do anything.
Difficult to know without seeing your project...
Here, though, is about as basic of a setup as possible.
Table view controller
cell prototype and class with a "row label" and a collection view
collection view prototype and class
Source for Storyboard:
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Ets-ss-TQd">
<device id="retina3_5" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="collection view cell content view" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Table Col Table View Controller-->
<scene sceneID="6jQ-dU-swf">
<objects>
<tableViewController id="Ets-ss-TQd" customClass="TableColTableViewController" customModule="MoreScratch" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="-1" estimatedSectionHeaderHeight="-1" sectionFooterHeight="-1" estimatedSectionFooterHeight="-1" id="zYt-i6-X2h">
<rect key="frame" x="0.0" y="0.0" width="320" height="480"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="someTableCell" rowHeight="125" id="B1r-57-a4y" customClass="SomeTableCell" customModule="MoreScratch" customModuleProvider="target">
<rect key="frame" x="0.0" y="44.5" width="320" height="125"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="B1r-57-a4y" id="Zyt-9U-dTe">
<rect key="frame" x="0.0" y="0.0" width="320" height="125"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="d9m-rN-Eyd">
<rect key="frame" x="16" y="54" width="288" height="60"/>
<color key="backgroundColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="60" id="E26-bE-U5T"/>
</constraints>
<collectionViewFlowLayout key="collectionViewLayout" scrollDirection="horizontal" minimumLineSpacing="10" minimumInteritemSpacing="4" id="mrH-RV-vqe">
<size key="itemSize" width="92" height="50"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="someCollectionCell" id="JEj-s8-EVA" customClass="SomeCollectionCell" customModule="MoreScratch" customModuleProvider="target">
<rect key="frame" x="0.0" y="5" width="92" height="50"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<collectionViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="mgI-0a-PJH">
<rect key="frame" x="0.0" y="0.0" width="92" height="50"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wJ1-UE-IvG">
<rect key="frame" x="8" y="8" width="76" height="34"/>
<color key="backgroundColor" red="0.92143100499999997" green="0.92145264149999995" blue="0.92144101860000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="wJ1-UE-IvG" firstAttribute="leading" secondItem="mgI-0a-PJH" secondAttribute="leadingMargin" id="7v0-S7-Kcb"/>
<constraint firstItem="wJ1-UE-IvG" firstAttribute="top" secondItem="mgI-0a-PJH" secondAttribute="topMargin" id="NUW-9U-OkK"/>
<constraint firstAttribute="bottomMargin" secondItem="wJ1-UE-IvG" secondAttribute="bottom" id="YXn-Qi-b3X"/>
<constraint firstAttribute="trailingMargin" secondItem="wJ1-UE-IvG" secondAttribute="trailing" id="ZNu-px-4Va"/>
</constraints>
</collectionViewCellContentView>
<connections>
<outlet property="label" destination="wJ1-UE-IvG" id="0fz-E6-bfF"/>
</connections>
</collectionViewCell>
</cells>
<connections>
<outlet property="dataSource" destination="B1r-57-a4y" id="hHd-RQ-xJb"/>
<outlet property="delegate" destination="B1r-57-a4y" id="3kW-TN-RKS"/>
</connections>
</collectionView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Row Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="GpB-tj-mDa">
<rect key="frame" x="16" y="11" width="288" height="35"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="d9m-rN-Eyd" secondAttribute="trailing" id="3Fd-ue-yEB"/>
<constraint firstAttribute="bottomMargin" secondItem="d9m-rN-Eyd" secondAttribute="bottom" priority="999" id="5EL-HQ-hli"/>
<constraint firstAttribute="trailingMargin" secondItem="GpB-tj-mDa" secondAttribute="trailing" id="UiZ-V8-tk0"/>
<constraint firstItem="d9m-rN-Eyd" firstAttribute="leading" secondItem="Zyt-9U-dTe" secondAttribute="leadingMargin" id="axO-Nr-EUG"/>
<constraint firstItem="GpB-tj-mDa" firstAttribute="leading" secondItem="Zyt-9U-dTe" secondAttribute="leadingMargin" id="hM3-MG-T66"/>
<constraint firstItem="d9m-rN-Eyd" firstAttribute="top" secondItem="GpB-tj-mDa" secondAttribute="bottom" constant="8" id="muE-Zg-mgg"/>
<constraint firstItem="GpB-tj-mDa" firstAttribute="top" secondItem="Zyt-9U-dTe" secondAttribute="topMargin" id="nea-19-Qvm"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="collectionView" destination="d9m-rN-Eyd" id="UOs-i9-xDd"/>
<outlet property="rowTitleLabel" destination="GpB-tj-mDa" id="LpD-4r-JgM"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="Ets-ss-TQd" id="RC7-c1-RLO"/>
<outlet property="delegate" destination="Ets-ss-TQd" id="sLA-mZ-FSY"/>
</connections>
</tableView>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="435-nV-YGQ" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="125.625" y="225"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
Classes:
import UIKit
struct SomeDataStruct {
var color: UIColor = .white
var num: Int = 0
}
class TableColTableViewController: UITableViewController {
var myData: [SomeDataStruct] = []
override func viewDidLoad() {
super.viewDidLoad()
let colors: [UIColor] = [
.systemRed, .systemGreen, .systemBlue, .cyan, .magenta, .yellow,
.red, .green, .blue, .orange, .brown, .purple,
]
let nums: [Int] = [
12, 15, 8, 21, 17, 14,
16, 10, 5, 13, 20, 19,
]
for (c, n) in zip(colors, nums) {
let d = SomeDataStruct(color: c, num: n)
myData.append(d)
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "someTableCell", for: indexPath) as! SomeTableCell
cell.rowTitleLabel.text = "Row \(indexPath.row)"
cell.thisData = myData[indexPath.row]
return cell
}
}
class SomeTableCell: UITableViewCell {
#IBOutlet var rowTitleLabel: UILabel!
#IBOutlet var collectionView: UICollectionView!
var thisData: SomeDataStruct = SomeDataStruct() {
didSet {
collectionView.reloadData()
}
}
}
extension SomeTableCell: UICollectionViewDataSource, UICollectionViewDelegate {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return thisData.num
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "someCollectionCell", for: indexPath) as! SomeCollectionCell
cell.label.text = "Cell \(indexPath.item)"
cell.contentView.backgroundColor = thisData.color
return cell
}
}
class SomeCollectionCell: UICollectionViewCell {
#IBOutlet var label: UILabel!
}
Result:
I'm trying to add a menu to the app menu through Appkit by using the NSMenu Class. But I'm not sure how to call it so far I've tried:
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
var menu: NSMenu!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 512),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
class myMenu: NSMenu {
init(aTitle: "HelloWorld")
}
}
But this only gives me "Expected parameter type following ':'" and "'required' initializer 'init(coder:)' must be provided by subclass of 'NSMenu'".
I use separate functions for buildWnd and buildMenu, but this should work using your technique. Create a new file in your swift project called main.swift and delete the pre-existing AppDelegate. Copy/paste this code into the main.swift file.
import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
// var menu: NSMenu!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
// let contentView = ContentView()
// Create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 512),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
window.center()
window.setFrameAutosaveName("Main Window")
// window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
let mainMenu = NSMenu()
NSApp.mainMenu = mainMenu
// **** App menu **** //
let appMenuItem = NSMenuItem()
mainMenu.addItem(appMenuItem)
let appMenu = NSMenu()
appMenuItem.submenu = appMenu
appMenu.addItem(withTitle:"Quit", action:#selector(NSApplication.terminate), keyEquivalent: "q")
}
}
let appDelegate = AppDelegate()
// **** Main **** //
let application = NSApplication.shared
application.delegate = appDelegate
//application.activate(ignoringOtherApps:true)
application.run()
I am trying to animate a change of color of a navigation bar when popping back to a previous controller. To give it some context, I have controller A which is a collectionView Controller and has an opaque navigation bar color set by:
self.navigationController?.navigationBar.barTintColor = UIColor.rgb(red: 244, green: 67, blue: 54)
self.navigationController?.navigationBar.tintColor = .white
self.navigationController?.navigationBar.titleTextAttributes = [NSForegroundColorAttributeName : UIColor.white]
self.navigationController?.navigationBar.isTranslucent = false
Once a collectionViewCell is selected I push to the next controller, B, where the navigation bar is altered to become transparent:
self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
self.navigationController?.navigationBar.shadowImage = UIImage()
self.navigationController?.navigationBar.isTranslucent = true
When a user taps on the back arrow, I want the navigationBar to return to it's original colors. I have tried a simple UIView animation on the controller B's viewWillDissappear function, willMove(toParentViewController) and the viewWillAppear function on Controller A:
Here is the animation:
UIView.animate(withDuration: 0.5) {
self.navigationController?.navigationBar.barTintColor = UIColor.rgb(red: 244, green: 67, blue: 54)
self.navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
self.navigationController?.navigationBar.shadowImage = nil
self.navigationController?.navigationBar.isTranslucent = false
self.navigationController?.navigationBar.layoutIfNeeded()
}
After doing this I tried using a transition coordinator but got the same results:
guard let coordinator = self.transitionCoordinator else {
return
}
coordinator.animate(alongsideTransition: {
[weak self] context in
self?.navigationController?.navigationBar.barTintColor = UIColor.rgb(red: 244, green: 67, blue: 54)
self?.navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
self?.navigationController?.navigationBar.shadowImage = nil
self?.navigationController?.navigationBar.isTranslucent = false
self?.navigationController?.navigationBar.layoutIfNeeded()
}, completion: nil)
It seems that no matter what I try, or where I put the code I always end up with the same outcome. I am aware that the code is repetitive, but I was just trying to figure out why, so a lot of copying and pasting occurred.
From what I can tell, I think that it has something to do with the background view of the previous controller, but I am stumped, I seem to see a black screen before the animation under the navigation bar. Any help would be greatly appreciated.
Thanks
Details
xCode 8.3.2, swift 3.1
Solution
override func viewWillAppear(_ animated: Bool) {
if let navigationBar = self.navigationController?.navigationBar {
navigationBar.backgroundColor = .blue
}
}
Full sample
ViewController
import UIKit
class ViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
if let navigationBar = self.navigationController?.navigationBar {
navigationBar.barTintColor = UIColor(red: 244/255, green: 67/255, blue: 54/255, alpha: 1.0)
navigationBar.tintColor = .white
navigationBar.titleTextAttributes = [NSForegroundColorAttributeName : UIColor.white]
navigationBar.isTranslucent = false
}
}
}
ViewController2
import UIKit
class ViewController2: UIViewController {
override func viewWillAppear(_ animated: Bool) {
if let navigationBar = self.navigationController?.navigationBar {
let color = UIColor(red: 1, green: 153/255, blue: 0, alpha: 1.0)
navigationBar.setBackgroundImage(UIImage.imageWithColor(color: color), for: .default)
navigationBar.shadowImage = UIImage()
navigationBar.isTranslucent = true
}
}
override func viewWillDisappear(_ animated: Bool) {
if let navigationBar = self.navigationController?.navigationBar {
navigationBar.setBackgroundImage(nil, for: .default)
navigationBar.shadowImage = nil
navigationBar.isTranslucent = false
}
}
}
extension UIImage
import UIKit
extension UIImage {
class func imageWithColor(color: UIColor) -> UIImage {
let rect = CGRect(x: 0, y: 0, width: 1, height: 1)
UIGraphicsBeginImageContextWithOptions(CGSize(width: 1, height: 1), false, 0)
color.setFill()
UIRectFill(rect)
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
}
Main.storyboard
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12120" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="Tzy-ol-uu0">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12088"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="stackowerflow_44343355" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eIC-Nm-Ex7">
<rect key="frame" x="164" y="318" width="46" height="30"/>
<state key="normal" title="Button"/>
<connections>
<segue destination="QHs-H4-fAS" kind="show" id="Rff-Eq-K6g"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="eIC-Nm-Ex7" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="G1g-VM-MAn"/>
<constraint firstItem="eIC-Nm-Ex7" firstAttribute="centerY" secondItem="8bC-Xf-vdC" secondAttribute="centerY" id="mdZ-GP-EQw"/>
</constraints>
</view>
<navigationItem key="navigationItem" id="hq3-zt-U4K"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="977" y="791"/>
</scene>
<!--View Controller2-->
<scene sceneID="F9C-Nz-6dd">
<objects>
<viewController id="QHs-H4-fAS" customClass="ViewController2" customModule="stackowerflow_44343355" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="AV0-X8-nhX"/>
<viewControllerLayoutGuide type="bottom" id="AsY-Gl-67v"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="1fA-pX-rzR">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Uzd-Tb-KRO" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1770" y="789"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="Jff-OO-3e7">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="Tzy-ol-uu0" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="804-YF-T6T">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="BYZ-38-t0r" kind="relationship" relationship="rootViewController" id="BRB-ym-7I2"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="M6i-ib-61I" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="140" y="791.15442278860576"/>
</scene>
</scenes>
</document>
I'm trying to get my CAGradientLayers, that i'm using to create nice gradient backgrounds, to resize nicely on rotation and modal view presentation, but they will not play ball.
Here is a video I just created showing my problem: Notice the tearing on rotation.
Also please note this video was created by filming the iPhone Simulator on OS X. I have slowed down the animations in the video to highlight my issue.
Video of Problem...
Here is an Xcode project which I just created (which is the source for the app shown in the video), basically as illustrated the issue occurs on rotation and especially when views are presented modally:
Xcode Project, modally presenting views with CAGradientLayer backgrounds...
For what it's worth I understand that using:
[[self view] setBackgroundColor:[UIColor blackColor]];
does a reasonable job of making the transitions a bit more seamless and less jarring, but if you look at the video when I, whilst currently in landscape mode, modally present a view, you will see why the above code will not help.
Any ideas what I can do to sort this out?
John
When you create a layer (like your gradient layer), there's no view managing the layer (even when you add it as a sublayer of some view's layer). A standalone layer like this doesn't participate in the UIView animation system.
So when you update the frame of the gradient layer, the layer animates the change with its own default animation parameters. (This is called “implicit animation”.) These default parameters don't match the animation parameters used for interface rotation, so you get a weird result.
I didn't look at your project but it's trivial to reproduce your problem with this code:
#interface ViewController ()
#property (nonatomic, strong) CAGradientLayer *gradientLayer;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.gradientLayer = [CAGradientLayer layer];
self.gradientLayer.colors = #[ (__bridge id)[UIColor blueColor].CGColor, (__bridge id)[UIColor blackColor].CGColor ];
[self.view.layer addSublayer:self.gradientLayer];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
self.gradientLayer.frame = self.view.bounds;
}
#end
Here's what that looks like, with slow motion enabled in the simulator:
Fortunately, this is an easy problem to fix. You need to make your gradient layer be managed by a view. You do that by creating a UIView subclass that uses a CAGradientLayer as its layer. The code is tiny:
// GradientView.h
#interface GradientView : UIView
#property (nonatomic, strong, readonly) CAGradientLayer *layer;
#end
// GradientView.m
#implementation GradientView
#dynamic layer;
+ (Class)layerClass {
return [CAGradientLayer class];
}
#end
Then you need to change your code to use GradientView instead of CAGradientLayer. Since you're using a view now instead of a layer, you can set the autoresizing mask to keep the gradient sized to its superview, so you don't have to do anything later to handle rotation:
#interface ViewController ()
#property (nonatomic, strong) GradientView *gradientView;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.gradientView = [[GradientView alloc] initWithFrame:self.view.bounds];
self.gradientView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.gradientView.layer.colors = #[ (__bridge id)[UIColor blueColor].CGColor, (__bridge id)[UIColor blackColor].CGColor ];
[self.view addSubview:self.gradientView];
}
#end
Here's the result:
The best part about #rob's answer is that the view controls the layer for you. Here is the Swift code that properly overrides the layer class and sets the gradient.
import UIKit
class GradientView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
private func setupView() {
autoresizingMask = [.flexibleWidth, .flexibleHeight]
guard let theLayer = self.layer as? CAGradientLayer else {
return;
}
theLayer.colors = [UIColor.whiteColor.cgColor, UIColor.lightGrayColor.cgColor]
theLayer.locations = [0.0, 1.0]
theLayer.frame = self.bounds
}
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
}
You can then add the view in two lines wherever you want.
override func viewDidLoad() {
super.viewDidLoad()
let gradientView = GradientView(frame: self.view.bounds)
self.view.insertSubview(gradientView, atIndex: 0)
}
My swift version:
import UIKit
class GradientView: UIView {
override class func layerClass() -> AnyClass {
return CAGradientLayer.self
}
func gradientWithColors(firstColor : UIColor, _ secondColor : UIColor) {
let deviceScale = UIScreen.mainScreen().scale
let gradientLayer = CAGradientLayer()
gradientLayer.frame = CGRectMake(0.0, 0.0, self.frame.size.width * deviceScale, self.frame.size.height * deviceScale)
gradientLayer.colors = [ firstColor.CGColor, secondColor.CGColor ]
self.layer.insertSublayer(gradientLayer, atIndex: 0)
}
}
Note that I also had to use the device scale to calculate the frame size - to get correct auto-sizing during orientation changes (with auto-layout).
In Interface Builder, I added a UIView and changed its class to GradientView (the class shown above).
I then created an outlet for it (myGradientView).
Finally, In the view controller I added:
override func viewDidLayoutSubviews() {
self.myGradientView.gradientWithColors(UIColor.whiteColor(), UIColor.blueColor())
}
Note that the gradient view is created in a "layoutSubviews" method, since we need a finalized frame to create the gradient layer.
It will look better when you insert this piece of code and remove the willAnimateRotationToInterfaceOrientation:duration: implementation.
- (void)viewWillLayoutSubviews
{
[[[self.view.layer sublayers] objectAtIndex:0] setFrame:self.view.bounds];
}
This is however not very elegant. In a real application you should subclass UIView to create a gradient view. In this custom view you can override layerClass so that it is backed by a gradient layer:
+ (Class)layerClass
{
return [CAGradientLayer class];
}
Also implement layoutSubviews to handle when the bounds of the view changes.
When creating this background view use autoresizing masks so that the bounds automatically adjust on interface rotations.
Complete Swift version. Set viewFrame from the viewController that owns this view in viewDidLayoutSubviews
import UIKit
class MainView: UIView {
let topColor = UIColor(red: 146.0/255.0, green: 141.0/255.0, blue: 171.0/255.0, alpha: 1.0).CGColor
let bottomColor = UIColor(red: 31.0/255.0, green: 28.0/255.0, blue: 44.0/255.0, alpha: 1.0).CGColor
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupGradient()
}
override class func layerClass() -> AnyClass {
return CAGradientLayer.self
}
var gradientLayer: CAGradientLayer {
return layer as! CAGradientLayer
}
var viewFrame: CGRect! {
didSet {
self.bounds = viewFrame
}
}
private func setupGradient() {
gradientLayer.colors = [topColor, bottomColor]
}
}
Info
Use as one line solution
Replacing gradient when you add it to the view again (to use in reusables)
Automatically transiting
Automatically removing
Details
Swift 3.1, xCode 8.3.3
Solution
import UIKit
extension UIView {
func addGradient(colors: [UIColor], locations: [NSNumber]) {
addSubview(ViewWithGradient(addTo: self, colors: colors, locations: locations))
}
}
class ViewWithGradient: UIView {
private var gradient = CAGradientLayer()
init(addTo parentView: UIView, colors: [UIColor], locations: [NSNumber]){
super.init(frame: CGRect(x: 0, y: 0, width: 1, height: 2))
restorationIdentifier = "__ViewWithGradient"
for subView in parentView.subviews {
if let subView = subView as? ViewWithGradient {
if subView.restorationIdentifier == restorationIdentifier {
subView.removeFromSuperview()
break
}
}
}
let cgColors = colors.map { (color) -> CGColor in
return color.cgColor
}
gradient.frame = parentView.frame
gradient.colors = cgColors
gradient.locations = locations
backgroundColor = .clear
parentView.addSubview(self)
parentView.layer.insertSublayer(gradient, at: 0)
parentView.backgroundColor = .clear
autoresizingMask = [.flexibleWidth, .flexibleHeight]
clipsToBounds = true
parentView.layer.masksToBounds = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
if let parentView = superview {
gradient.frame = parentView.bounds
}
}
override func removeFromSuperview() {
super.removeFromSuperview()
gradient.removeFromSuperlayer()
}
}
Usage
viewWithGradient.addGradient(colors: [.blue, .green, .orange], locations: [0.1, 0.3, 1.0])
Using StoryBoard
ViewController
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var viewWithGradient: UIView!
override func viewDidLoad() {
super.viewDidLoad()
viewWithGradient.addGradient(colors: [.blue, .green, .orange], locations: [0.1, 0.3, 1.0])
}
}
StoryBoard
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="stackoverflow_17555986" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="uii-31-sl9">
<rect key="frame" x="66" y="70" width="243" height="547"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
</view>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="wfy-db-euE" firstAttribute="top" secondItem="uii-31-sl9" secondAttribute="bottom" constant="50" id="a7J-Hq-IIq"/>
<constraint firstAttribute="trailingMargin" secondItem="uii-31-sl9" secondAttribute="trailing" constant="50" id="i9v-hq-4tD"/>
<constraint firstItem="uii-31-sl9" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" constant="50" id="wlO-83-8FY"/>
<constraint firstItem="uii-31-sl9" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leadingMargin" constant="50" id="zb6-EH-j6p"/>
</constraints>
</view>
<connections>
<outlet property="viewWithGradient" destination="uii-31-sl9" id="FWB-7A-MaH"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
Programmatically
import UIKit
class ViewController2: UIViewController {
#IBOutlet weak var viewWithGradient: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let viewWithGradient = UIView(frame: CGRect(x: 10, y: 20, width: 30, height: 40))
view.addSubview(viewWithGradient)
viewWithGradient.translatesAutoresizingMaskIntoConstraints = false
let constant:CGFloat = 50.0
NSLayoutConstraint(item: viewWithGradient, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leadingMargin, multiplier: 1.0, constant: constant).isActive = true
NSLayoutConstraint(item: viewWithGradient, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailingMargin
, multiplier: 1.0, constant: -1*constant).isActive = true
NSLayoutConstraint(item: viewWithGradient, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottomMargin
, multiplier: 1.0, constant: -1*constant).isActive = true
NSLayoutConstraint(item: viewWithGradient, attribute: .top, relatedBy: .equal, toItem: view, attribute: .topMargin
, multiplier: 1.0, constant: constant).isActive = true
viewWithGradient.addGradient(colors: [.blue, .green, .orange], locations: [0.1, 0.3, 1.0])
}
}
Another swift version - which is not using drawRect.
class UIGradientView: UIView {
override class func layerClass() -> AnyClass {
return CAGradientLayer.self
}
var gradientLayer: CAGradientLayer {
return layer as! CAGradientLayer
}
func setGradientBackground(colors: [UIColor], startPoint: CGPoint = CGPoint(x: 0.5, y: 0), endPoint: CGPoint = CGPoint(x: 0.5, y: 1)) {
gradientLayer.startPoint = startPoint
gradientLayer.endPoint = endPoint
gradientLayer.colors = colors.map({ (color) -> CGColor in return color.CGColor })
}
}
In controller I just call:
gradientView.setGradientBackground([UIColor.grayColor(), UIColor.whiteColor()])
Personally, I prefer to keep everything self-contained within the view subclass.
Here's my Swift implementation:
import UIKit
#IBDesignable
class GradientBackdropView: UIView {
#IBInspectable var startColor: UIColor=UIColor.whiteColor()
#IBInspectable var endColor: UIColor=UIColor.whiteColor()
#IBInspectable var intermediateColor: UIColor=UIColor.whiteColor()
var gradientLayer: CAGradientLayer?
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
// Drawing code
super.drawRect(rect)
if gradientLayer == nil {
self.addGradientLayer(rect: rect)
} else {
gradientLayer?.removeFromSuperlayer()
gradientLayer=nil
self.addGradientLayer(rect: rect)
}
}
override func layoutSubviews() {
super.layoutSubviews()
if gradientLayer == nil {
self.addGradientLayer(rect: self.bounds)
} else {
gradientLayer?.removeFromSuperlayer()
gradientLayer=nil
self.addGradientLayer(rect: self.bounds)
}
}
func addGradientLayer(rect rect:CGRect) {
gradientLayer=CAGradientLayer()
gradientLayer?.frame=self.bounds
gradientLayer?.colors=[startColor.CGColor,intermediateColor.CGColor,endColor.CGColor]
gradientLayer?.startPoint=CGPointMake(0.0, 1.0)
gradientLayer?.endPoint=CGPointMake(0.0, 0.0)
gradientLayer?.locations=[NSNumber(float: 0.1),NSNumber(float: 0.5),NSNumber(float: 1.0)]
self.layer.insertSublayer(gradientLayer!, atIndex: 0)
gradientLayer?.transform=self.layer.transform
}
}
You can use this from storyboard, xib or code. You can change the colors dynamically later (I needed that for my case)
Adding a complete copy-pastable one here:
import UIKit
class GradientView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
private func setupView() {
autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
}
extension GradientView {
func setVerticalGradientBackground(colors: [CGColor], locations: [CGFloat] = [0, 1]) {
setGradientBackground(colors: colors, locations: locations, startPoint: .init(x: 0.5, y: 0), endPoint: .init(x: 0.5, y: 1))
}
func setHorizontalGradientBackground(colors: [CGColor], locations: [CGFloat] = [0, 1]) {
setGradientBackground(colors: colors, locations: locations, startPoint: .init(x: 0, y: 0.5), endPoint: .init(x: 1, y: 0.5))
}
func setGradientBackground(colors: [CGColor],
locations: [CGFloat],
startPoint: CGPoint,
endPoint: CGPoint) {
guard let gradientLayer = self.layer as? CAGradientLayer else {
return
}
gradientLayer.colors = colors
gradientLayer.locations = locations.map { $0 as NSNumber }
gradientLayer.startPoint = startPoint
gradientLayer.endPoint = endPoint
gradientLayer.frame = bounds
}
}
Easy way. You can add a gradient layer each time when your view changes its size:
class YourVC: UIViewController {
...
override func viewDidLoad() {
super.viewDidLoad()
yourView.addObserver(self, forKeyPath: "bounds", options: [], context: nil)
}
...
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (object as? NSObject == yourView && keyPath == "bounds") {
//remove and add again your gradient layer
}
}
...