i have a cell, that contains containerView with top and bottom cornerRadius = 8.
Then i have to put UIImageView with contentMode = .scaleAspectFit and corner radius ONLY on the top (cornerRadius = 8)
But the problem that code 'corner radius' is not working with contentMode = .scaleAspectFit
Here is properties
let containerView: UIView = {
let view = UIView()
view.layer.cornerRadius = 8
return view
}()
let imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.layer.cornerRadius = 8
imageView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
imageView.clipsToBounds = true
return imageView
}()
Here is SnapKit method
private func setupViews() {
addSubview(containerView)
containerView.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.top.bottom.equalToSuperview()
}
containerView.addSubview(imageView)
imageView.snp.makeConstraints { make in
make.top.equalTo(containerView.snp.top)
make.trailing.equalTo(containerView.snp.trailing)
make.leading.equalTo(containerView.snp.leading)
}
}
as a result i got , i need my image.top EqualToContainer.top and with cornerRadius on the top
You haven't given the image view a height constraint, and a UIImageView has no intrinsic size until its .image has been set.
So, when you set the image, the image view will use the height of the image to set its own height. Then, because you're telling it to use .scaleAspectFit, you get the image centered in the new height.
What you need to do is leave the image view at the default of .scaleToFill and then set its height constraint to use the same proportion as the image.
A quick example, using these two images:
We want it to look like this (I've exaggerated the corner radius to make it obvious):
We'll use a UIView subclass -- but the same thing will apply when using this in a cell:
class AspectView: UIView {
public var image: UIImage? {
didSet {
// set the image view's image
imageView.image = image
// if height constraint is already set
if let h = hConstraint {
// deactivate it
h.isActive = false
}
// set new height constraint to image aspect ratio
imageView.snp.makeConstraints { make in
self.hConstraint = make.height.equalTo(imageView.snp.width).multipliedBy(image!.size.height / image!.size.width).constraint
}
}
}
private var hConstraint: Constraint!
private let containerView: UIView = {
let view = UIView()
// using corner radius of 32 to make it very obvious
view.layer.cornerRadius = 32
// this will clip the image view subview
view.clipsToBounds = true
return view
}()
private let imageView: UIImageView = {
let imageView = UIImageView()
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupViews()
}
public func addImage(_ img: UIImage) {
imageView.image = img
print(img.size)
if let h = hConstraint {
h.isActive = false
}
imageView.snp.makeConstraints { make in
self.hConstraint = make.height.equalTo(imageView.snp.width).multipliedBy(img.size.height / img.size.width).constraint
}
}
private func setupViews() {
addSubview(containerView)
containerView.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.top.bottom.equalToSuperview()
}
containerView.addSubview(imageView)
imageView.snp.makeConstraints { make in
make.top.equalTo(containerView.snp.top)
make.trailing.equalTo(containerView.snp.trailing)
make.leading.equalTo(containerView.snp.leading)
}
// so we can see the containerView frame
containerView.backgroundColor = .systemBlue
}
}
and a sample controller class - tapping anywhere will toggle between the two images:
class AspectTestVC: UIViewController {
let testView = AspectView()
let imgNames: [String] = [
"p320x160",
"p320x240",
]
var imgIdx: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
// bkg640x360
view.addSubview(testView)
testView.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview().inset(40.0)
make.height.equalTo(400.0)
}
if let img = UIImage(named: imgNames[imgIdx % imgNames.count]) {
testView.image = img
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
imgIdx += 1
if let img = UIImage(named: imgNames[imgIdx % imgNames.count]) {
testView.image = img
}
}
}
Related
I am returning a single UILabel within a UIImageView, within a Cell for a UICollectionViewController. My didSelectItem and dynamic width or working for the cell.
However I am trying to make the UIImageView a capsule shape, the below code I thought would return a circle but isn't. What I want to make is a capsule background layer for the Text, similar to a button but not a button.
class CategoryView: UIView {
private let capsuleBackground: UIImageView = {
let view = UIImageView()
view.layer.backgroundColor = UIColor.white.cgColor
view.layer.borderWidth = 1
view.layer.borderColor = COLOR_PRIMARY?.cgColor
view.layer.cornerRadius = view.frame.size.width/2
view.translatesAutoresizingMaskIntoConstraints = false
view.clipsToBounds = true
view.isUserInteractionEnabled = false
return view
}()
private let textLabel: CustomUILabel = {
let label = CustomUILabel(title: "Hello World")
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
fileprivate func setup() {
addSubview(capsuleBackground)
capsuleBackground.addSubview(textLabel)
capsuleBackground.centerInSuperview()
textLabel.edges(to: capsuleBackground, insets: TinyEdgeInsets(top: 8, left: 8, bottom: 8, right: 8))
}
}
this is what it looks like
view.layer.cornerRadius = 12
🤦♂️
UIKit/Programmatic UI
I have an UIScrollView with an UIImageView inside. The image is set by user selection and can have all kinds of sizes. What I want is that the image initially fills the screen (view) and then can be zoomed and scrolled all the way to the edges of the image.
If I add the ImageView directly to the view (no scrollView), I get it to fill the screen with the following code:
mapImageView.image = ProjectImages.projectDefaultImage
mapImageView.translatesAutoresizingMaskIntoConstraints = false
mapImageView.contentMode = .scaleAspectFill
view.addSubview(mapImageView)
Now the same with the scrollView and the embedded imageView:
view.insertSubview(mapImageScrollView, at: 0)
mapImageScrollView.delegate = self
mapImageScrollView.translatesAutoresizingMaskIntoConstraints = false
mapImageScrollView.contentMode = .scaleAspectFill
mapImageScrollView.maximumZoomScale = 4.0
mapImageScrollView.pinToEdges(of: view, safeArea: true)
mapImageView.image = ProjectImages.projectDefaultImage
mapImageView.translatesAutoresizingMaskIntoConstraints = false
mapImageView.contentMode = .scaleAspectFill
mapImageScrollView.addSubview(mapImageView)
And now, if the image's height is smaller than the view's height, the image does not fill the screen and I'm left with a blank view area below the image. I can zoom and scroll ok, and then the image does fill the view.
Adding contsraints will fill the view as I want, but interferes with the zooming and scrolling and prevents me getting to the edges of the image when zoomed in.
How to set this up correctly ?
You might find this useful...
It allows you to zoom an image in a scrollView, starting with it centered and maintaining aspect ratio.
Here's a complete implementation. It has two important variables at the top:
// can be .scaleAspectFill or .scaleAspectFit
var fitMode: UIView.ContentMode = .scaleAspectFill
// if fitMode is .scaleAspectFit, allowFullImage is ignored
// if fitMode is .scaleAspectFill, image will start zoomed to .scaleAspectFill
// if allowFullImage is false, image will zoom back to .scaleAspectFill if "pinched in"
// if allowFullImage is true, image can be "pinched in" to see the full image
var allowFullImage: Bool = true
Everything is done via code - no #IBOutlet or other connections - so just create add a new view controller and assign its custom class to ZoomAspectViewController (and edit the name of the image you want to use):
class ZoomAspectViewController: UIViewController, UIScrollViewDelegate {
var scrollView: UIScrollView!
var imageView: UIImageView!
var imageViewBottomConstraint: NSLayoutConstraint!
var imageViewLeadingConstraint: NSLayoutConstraint!
var imageViewTopConstraint: NSLayoutConstraint!
var imageViewTrailingConstraint: NSLayoutConstraint!
// can be .scaleAspectFill or .scaleAspectFit
var fitMode: UIView.ContentMode = .scaleAspectFit
// if fitMode is .scaleAspectFit, allowFullImage is ignored
// if fitMode is .scaleAspectFill, image will start zoomed to .scaleAspectFill
// if allowFullImage is false, image will zoom back to .scaleAspectFill if "pinched in"
// if allowFullImage is true, image can be "pinched in" to see the full image
var allowFullImage: Bool = true
override func viewDidLoad() {
super.viewDidLoad()
guard let img = UIImage(named: "myImage") else {
fatalError("Could not load the image!!!")
}
scrollView = UIScrollView()
imageView = UIImageView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleToFill
scrollView.addSubview(imageView)
view.addSubview(scrollView)
// respect safe area
let g = view.safeAreaLayoutGuide
imageViewTopConstraint = imageView.topAnchor.constraint(equalTo: scrollView.topAnchor)
imageViewBottomConstraint = imageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor)
imageViewLeadingConstraint = imageView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor)
imageViewTrailingConstraint = imageView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: g.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
imageViewTopConstraint,
imageViewBottomConstraint,
imageViewLeadingConstraint,
imageViewTrailingConstraint,
])
scrollView.delegate = self
scrollView.minimumZoomScale = 0.1
scrollView.maximumZoomScale = 5.0
imageView.image = img
imageView.frame.size = img.size
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { _ in
self.updateMinZoomScaleForSize(size, shouldSize: (self.scrollView.zoomScale == self.scrollView.minimumZoomScale))
self.updateConstraintsForSize(size)
}, completion: {
_ in
})
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updateMinZoomScaleForSize(scrollView.bounds.size)
updateConstraintsForSize(scrollView.bounds.size)
if fitMode == .scaleAspectFill {
centerImageView()
}
}
func updateMinZoomScaleForSize(_ size: CGSize, shouldSize: Bool = true) {
guard let img = imageView.image else {
return
}
var bShouldSize = shouldSize
let widthScale = size.width / img.size.width
let heightScale = size.height / img.size.height
var minScale = min(widthScale, heightScale)
let startScale = max(widthScale, heightScale)
if fitMode == .scaleAspectFill && !allowFullImage {
minScale = startScale
}
if scrollView.zoomScale < minScale {
bShouldSize = true
}
scrollView.minimumZoomScale = minScale
if bShouldSize {
scrollView.zoomScale = fitMode == .scaleAspectFill ? startScale : minScale
}
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
updateConstraintsForSize(scrollView.bounds.size)
}
func centerImageView() -> Void {
let yOffset = (scrollView.frame.size.height - imageView.frame.size.height) / 2
let xOffset = (scrollView.frame.size.width - imageView.frame.size.width) / 2
scrollView.contentOffset = CGPoint(x: -xOffset, y: -yOffset)
}
func updateConstraintsForSize(_ size: CGSize) {
let yOffset = max(0, (size.height - imageView.frame.height) / 2)
imageViewTopConstraint.constant = yOffset
imageViewBottomConstraint.constant = yOffset
let xOffset = max(0, (size.width - imageView.frame.width) / 2)
imageViewLeadingConstraint.constant = xOffset
imageViewTrailingConstraint.constant = xOffset
view.layoutIfNeeded()
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
}
Edit
As an example, I used this image (2560 x 1440):
and I get this result on launch:
and maximum zoom in (5.0) scrolled to top-center:
Edit 2
Same image, at launch, with:
var fitMode: UIView.ContentMode = .scaleAspectFill
instead of .scaleAspectFit:
I've found this solution that works for me, when setting and changing the image, I calculate the minimum needed zoom scale and set it on the scrollView:
var selectedMapImage: MapImage? {
didSet {
mapImageView.image = mapImagesController.getImageForMapImage(selectedMapImage!)
mapImageScrollView.minimumZoomScale = view.bounds.height / mapImageView.image!.size.height
mapImageScrollView.setZoomScale(mapImageScrollView.minimumZoomScale, animated: true)
mapImageScrollView.scrollRectToVisible(view.bounds, animated: true)
}
}
I have a need for a simple QR Code class that I can re-use. I have created the class and it works, however manually setting the size constraints is not desired because it needs to adjust its size based on the DPI of the device. Here in this minimal example, I just use 100 as the sizing calculation code is not relevant (set to 50 in IB). Also I will have multiple QR Codes in different positions, which I will manage their positioning by IB. But at least I hope to be able to set the width and height constraints in code.
The below code shows a QR code, in the right size (set at runtime), but when the constraints are set to horizontally and vertically center it, it does not. Again, I don't want the size constraints in the IB, but I do want the position constraints in the IB
import Foundation
import UIKit
#IBDesignable class QrCodeView: UIImageView {
var content:String = "test" {
didSet {
generateCode(content)
}
}
lazy var filter = CIFilter(name: "CIQRCodeGenerator")
lazy var imageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override func layoutSubviews() {
super.layoutSubviews()
imageView.frame = CGRect(x:0, y:0, width:100, height:100)
frame = CGRect(x:frame.origin.x, y:frame.origin.y, width:100, height:100)
}
func setup() {
//translatesAutoresizingMaskIntoConstraints = false
generateCode(content)
addSubview(imageView)
layoutIfNeeded()
}
func generateCode(_ string: String) {
guard let filter = filter,
let data = string.data(using: .isoLatin1, allowLossyConversion: false) else {
return
}
filter.setValue(data, forKey: "inputMessage")
guard let ciImage = filter.outputImage else {
return
}
let transform = CGAffineTransform(scaleX: 10, y: 10)
let scaled = UIImage(ciImage: ciImage.transformed(by: transform))
imageView.image = scaled
}
}
I believe you're making this more complicated than need be...
Let's start with a simple #IBDesignable UIImageView subclass.
Start with a new project and add this code:
#IBDesignable
class MyImageView: UIImageView {
// we'll use this later
var myIntrinsicSize: CGSize = CGSize(width: 100.0, height: 100.0)
override var intrinsicContentSize: CGSize {
return myIntrinsicSize
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
setup()
self.image = UIImage()
}
func setup() {
backgroundColor = .green
contentMode = .scaleToFill
}
}
Now, in Storyboard, add a UIImageView to a view controller. Set its custom class to MyImageView and set Horizontal and Vertical Center constraints.
The image view should automatically size itself to 100 x 100, centered in the view with a green background (we're just setting the background so we can see it):
Run the app, and you should see the same thing.
Now, add it as an #IBOutlet to a view controller:
class ViewController: UIViewController {
#IBOutlet var testImageView: MyImageView!
override func viewDidLoad() {
super.viewDidLoad()
testImageView.myIntrinsicSize = CGSize(width: 300.0, height: 300.0)
}
}
Run the app, and you will see a centered green image view, but now it will be 300 x 300 points instead of 100 x 100.
The rest of your task is pretty much adding code to set this custom class's .image property once you've rendered the QRCode image.
Here's the custom class:
#IBDesignable
class QRCodeView: UIImageView {
// so we can test changing the QRCode content in IB
#IBInspectable
var content:String = "test" {
didSet {
generateCode(content)
}
}
var qrIntrinsicSize: CGSize = CGSize(width: 100.0, height: 100.0)
override var intrinsicContentSize: CGSize {
return qrIntrinsicSize
}
lazy var filter = CIFilter(name: "CIQRCodeGenerator")
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
setup()
generateCode(content)
}
func setup() {
contentMode = .scaleToFill
}
override func layoutSubviews() {
super.layoutSubviews()
generateCode(content)
}
func generateCode(_ string: String) {
guard let filter = filter,
let data = string.data(using: .isoLatin1, allowLossyConversion: false) else {
return
}
filter.setValue(data, forKey: "inputMessage")
guard let ciImage = filter.outputImage else {
return
}
let scX = bounds.width / ciImage.extent.size.width
let scY = bounds.height / ciImage.extent.size.height
let transform = CGAffineTransform(scaleX: scX, y: scY)
let scaled = UIImage(ciImage: ciImage.transformed(by: transform))
self.image = scaled
}
}
In Storyboard / IB:
And here's an example view controller:
class ViewController: UIViewController {
#IBOutlet var qrCodeView: QRCodeView!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// calculate your needed size
// I'll assume it ended up being 240 x 240
qrCodeView.qrIntrinsicSize = CGSize(width: 240.0, height: 240.0)
}
}
Edit
Here's a modified QRCodeView class that will size itself to a (physical) 15x15 mm image.
I used DeviceKit from https://github.com/devicekit/DeviceKit to get the current device's ppi. See the comment to replace it with your own (assuming you are already using something else).
When this class is instantiated, it will:
get the current device's ppi
convert ppi to pixels-per-millimeter
calculate 15 x pixels-per-millimeter
convert based on screen scale
update its intrinsic size
The QRCodeView (subclass of UIImageView) needs only position constraints... so you can use Top + Leading, Top + Trailing, Center X & Y, Bottom + CenterX, etc, etc.
#IBDesignable
class QRCodeView: UIImageView {
#IBInspectable
var content:String = "test" {
didSet {
generateCode(content)
}
}
var qrIntrinsicSize: CGSize = CGSize(width: 100.0, height: 100.0)
override var intrinsicContentSize: CGSize {
return qrIntrinsicSize
}
lazy var filter = CIFilter(name: "CIQRCodeGenerator")
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
setup()
generateCode(content)
}
func setup() {
contentMode = .scaleToFill
// using DeviceKit from https://github.com/devicekit/DeviceKit
// replace with your lookup code that gets
// the device's ppi
let device = Device.current
guard let ppi = device.ppi else { return }
// convert to pixels-per-millimeter
let ppmm = CGFloat(ppi) / 25.4
// we want 15mm size
let mm15 = 15.0 * ppmm
// convert based on screen scale
let mmScale = mm15 / UIScreen.main.scale
// update our intrinsic size
self.qrIntrinsicSize = CGSize(width: mmScale, height: mmScale)
}
override func layoutSubviews() {
super.layoutSubviews()
generateCode(content)
}
func generateCode(_ string: String) {
guard let filter = filter,
let data = string.data(using: .isoLatin1, allowLossyConversion: false) else {
return
}
filter.setValue(data, forKey: "inputMessage")
guard let ciImage = filter.outputImage else {
return
}
let scX = bounds.width / ciImage.extent.size.width
let scY = bounds.height / ciImage.extent.size.height
let transform = CGAffineTransform(scaleX: scX, y: scY)
let scaled = UIImage(ciImage: ciImage.transformed(by: transform))
self.image = scaled
}
}
After struggling with programmatic dynamic UI elements for the last few weeks I decided I would give UIstackView a try.
I want custom UIView classes to occupy the stackview with different heights based upon user Input I would remove, add views on the fly.
I found out that stackViews base their 'cell' height upon the UI element's intrinsic content size. However, UIViews do not have one. I searched far and wide and found out that I need to override the View's intrisicContentsize function with one where I can explicitly set the width and height.
However results are very unpredictable and I'm sure there is some little thing that I just do not know dat causes this weird behaviour. Since I'm new to the language and there are a LOT of gotcha's I'm just gonna paste the code here and hope you'll be able to spot what I'm doing wrong.
I've read the docs ofc, a lot of articles, they all point to that override funcion that does not seem to work form me.
This is my mainViewController class;
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
var bv = BackButtonView(frame: CGRect.zero, image: UIImage(named: "backArrow.png")!)
var redView : UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
return view
}();
var blueView : UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .blue
return view
}();
let stack : UIStackView = {
let stack = UIStackView();
stack.translatesAutoresizingMaskIntoConstraints = false;
stack.axis = .vertical
stack.distribution = .fillProportionally;
stack.spacing = 8
return stack;
}();
override func viewDidLoad() {
let view = UIView(frame: UIScreen.main.bounds)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
self.view = view
self.view.addSubview(stack)
createLayout()
bv.intrinsicContentSize
stack.addArrangedSubview(bv)
print(bv.frame)
print(bv.intrinsicContentSize)
stack.layoutIfNeeded()
stack.addArrangedSubview(redView)
stack.addArrangedSubview(blueView)
}
private func setConstraints(view: UIView) -> [NSLayoutConstraint] {
return [
view.heightAnchor.constraint(equalToConstant: 50),
]
}
private func createLayout() {
stack.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
stack.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
stack.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
stack.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
}
}
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = MyViewController()
Now here's my custom UIClass that is giving me all this trouble:
import UIKit
public class BackButtonView : UIView {
public var size = CGSize(width: 10, height: UIView.noIntrinsicMetric)
override open var intrinsicContentSize: CGSize {
return size
}
var button : UIButton;
public init(frame: CGRect, image: UIImage) {
button = UIButton()
super.init(frame : frame);
self.translatesAutoresizingMaskIntoConstraints = false;
self.backgroundColor = .black
self.addSubview(button)
setupButton(image: image);
print(button.intrinsicContentSize)
}
let backButtonTrailingPadding : CGFloat = -18
lazy var buttonConstraints = [
button.heightAnchor.constraint(equalToConstant: 50),
button.widthAnchor.constraint(equalToConstant: 50),
button.centerYAnchor.constraint(equalTo: self.centerYAnchor),
button.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: backButtonTrailingPadding),
]
private func setupButton(image: UIImage) {
self.button.translatesAutoresizingMaskIntoConstraints = false;
self.button.setImage(image, for: .normal);
self.button.contentMode = .scaleToFill
NSLayoutConstraint.activate(buttonConstraints)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
struct anchors {
var topAnchor = NSLayoutConstraint()
var bottomAnchor = NSLayoutConstraint()
var leadingAnchor = NSLayoutConstraint()
var trailingAnchor = NSLayoutConstraint()
}
}
You can see that the specified width in the size proprrty gets ignored.
Under the above conditions., this is the output
If I now change the my custom UIClass's intrisiContentSize height to anyting else, this happens:
public var size = CGSize(width: UIView.noIntrinsicMetric, height: 10)
override open var intrinsicContentSize: CGSize {
return size
}
result:
Please help me figure out what is and isn't going on
The final screen should look something like this:
How can I mask an UIView or UIImageView so that a text is cutout from it?
I googled a lot and it seems that many people struggled the same. Most irritating I always tried to invert the alpha of a snapshotted view to get the result.
What I want looks like this:
This is the custom mask label.
import UIKit
final class MaskLabel: UILabel {
// MARK: - IBInspectoable
#IBInspectable var cornerRadius: CGFloat {
get { return self.layer.cornerRadius }
set { self.layer.cornerRadius = newValue }
}
#IBInspectable var borderWidth: CGFloat {
get { return self.layer.cornerRadius }
set { self.layer.borderWidth = newValue }
}
#IBInspectable var borderColor: UIColor {
get { return UIColor(cgColor: self.layer.borderColor ?? UIColor.clear.cgColor) }
set { self.layer.borderColor = newValue.cgColor }
}
#IBInspectable var insetTop: CGFloat {
get { return self.textInsets.top }
set { self.textInsets.top = newValue }
}
#IBInspectable var insetLeft: CGFloat {
get { return self.textInsets.left }
set { self.textInsets.left = newValue }
}
#IBInspectable var insetBottom: CGFloat {
get { return self.textInsets.bottom }
set { self.textInsets.bottom = newValue }
}
#IBInspectable var insetRight: CGFloat {
get { return self.textInsets.right }
set { self.textInsets.right = newValue }
}
// MARK: - Value
// MARK: Public
private var textInsets = UIEdgeInsets.zero
private var originalBackgroundColor: UIColor? = nil
// MARK: - Initializer
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setLabelUI()
}
override init(frame: CGRect) {
super.init(frame: frame)
setLabelUI()
}
override func prepareForInterfaceBuilder() {
setLabelUI()
}
// MARK: - Draw
override func drawText(in rect: CGRect) {
super.drawText(in: UIEdgeInsetsInsetRect(rect, textInsets))
guard let context = UIGraphicsGetCurrentContext() else { return }
context.saveGState()
context.setBlendMode(.clear)
originalBackgroundColor?.setFill()
UIRectFill(rect)
super.drawText(in: rect)
context.restoreGState()
}
// MARK: - Function
// MARK: Private
private func setLabelUI() {
// cache (Before masking the label, the background color must be clear. So we have to cache it)
originalBackgroundColor = backgroundColor
backgroundColor = .clear
layer.cornerRadius = cornerRadius
layer.borderWidth = borderWidth
layer.borderColor = borderColor.cgColor
}
}
This is the result.
The main problem I had was my understanding. Instead of taking a colored view and trying to make a transparent hole in it, we can just layer it the other way around.
So we have the colored background in the back, followed by the image in front that has the mask on it to only show the text part. And actually, that's pretty simple if you're using iOS 8+ by using the maskView property of UIView.
So it could look something like this in swift:
let coloredBackground = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 100))
coloredBackground.backgroundColor = UIColor.greenColor()
let imageView = UIImageView(frame: coloredBackground.bounds)
imageView.image = UIImage(named: "myImage")
coloredBackground.addSubview(imageView)
let label = UILabel(frame: coloredBackground.bounds)
label.text = "stackoverflow"
coloredBackground.addSubview(label)
imageView.maskView = label