I have an inheritance problem that I'm trying to solve. Typically, I'd just use multi-inheritance here, but Swift doesn't really do that.
Custom UIView
import UIKit
class ValidationView: UIView {
var required:Bool = false
var validRegex:String? = nil
var requiredLbl:UILabel?
private var requiredColor:UIColor = UIColor.red
private var requiredText:String = "*"
private var requiredFont:UIFont = UIFont.systemFont(ofSize: 16.0, weight: UIFont.Weight.bold)
override init(frame: CGRect) {
super.init(frame: frame)
self.setupValidationViews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setupValidationViews()
}
private func setupValidationViews() {
self.requiredLbl = UILabel(frame: CGRect(x: self.frame.width - 30, y: 30, width: 20, height: 20))
self.styleRequiredLabel()
self.addSubview(self.requiredLbl!)
}
func styleRequiredLabel(color:UIColor?, text:String?, font:UIFont?) {
self.requiredColor = color ?? self.requiredColor
self.requiredText = text ?? self.requiredText
self.requiredFont = font ?? self.requiredFont
self.styleRequiredLabel()
}
private func styleRequiredLabel() {
self.requiredLbl?.textColor = self.requiredColor
self.requiredLbl?.text = self.requiredText
self.requiredLbl?.font = self.requiredFont
}
}
Custom UITextField
import Foundation
import UIKit
#IBDesignable open class CustomTextField: UITextField {
#IBInspectable public var borderWidth: CGFloat = 2.0 {
didSet {
layer.borderWidth = borderWidth
}
}
#IBInspectable public var borderColor: UIColor = UIColor.lightGray {
didSet {
layer.borderColor = borderColor.cgColor
}
}
#IBInspectable public var cornerRadius: CGFloat = 4.0 {
didSet {
layer.cornerRadius = cornerRadius
layer.masksToBounds = true
}
}
}
I want that Custom UITextField to also be a ValidationView. I know I could do a protocol and extension and then have my CustomTextField implement that protocol, but that doesn't allow for init overrides. I'd rather not have to change the inits on ever view that implements ValidationView.
Something like this can be accomplished using #arturdev answer. I ended up with this:
import UIKit
class ValidatableProperties {
var required:Bool
var validRegex:String?
var requiredColor:UIColor
var requiredText:String
var requiredFont:UIFont
init(required:Bool, validRegex:String?, requiredColor:UIColor, requiredText:String, requiredFont:UIFont) {
self.required = required
self.validRegex = validRegex
self.requiredText = requiredText
self.requiredColor = requiredColor
self.requiredFont = requiredFont
}
}
protocol Validatable : UIView {
var validatableProperties:ValidatableProperties! { get set }
var requiredLbl:UILabel! { get set }
func setupValidationDefaults()
func setupValidationViews(frame:CGRect)
func styleRequiredLabel(color:UIColor?, text:String?, font:UIFont?)
}
extension Validatable {
func setupValidationDefaults() {
let props = ValidatableProperties(required: false, validRegex: nil, requiredColor: UIColor.red, requiredText: "*", requiredFont: UIFont.systemFont(ofSize: 16.0, weight: .bold))
self.validatableProperties = props
}
func setupValidationViews(frame:CGRect) {
self.requiredLbl = UILabel(frame: CGRect(x: frame.width, y: 0, width: 20, height: 20))
self.styleRequiredLabel()
self.addSubview(self.requiredLbl)
}
func styleRequiredLabel(color:UIColor?, text:String?, font:UIFont?) {
self.validatableProperties.requiredColor = color ?? self.validatableProperties.requiredColor
self.validatableProperties.requiredText = text ?? self.validatableProperties.requiredText
self.validatableProperties.requiredFont = font ?? self.validatableProperties.requiredFont
self.styleRequiredLabel()
}
private func styleRequiredLabel() {
self.requiredLbl.textColor = self.validatableProperties.requiredColor
self.requiredLbl.text = self.validatableProperties.requiredText
self.requiredLbl.font = self.validatableProperties.requiredFont
}
}
open class ValidationTextField:UITextField, Validatable {
var requiredLbl: UILabel!
var validatableProperties: ValidatableProperties!
override public init(frame: CGRect) {
super.init(frame: frame)
self.setupValidationDefaults()
self.setupValidationViews(frame: frame)
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setupValidationDefaults()
self.setupValidationViews(frame: self.frame)
}
}
But this requires extending all classes you want validatable into their own custom classes, needing to overwrite the inits every time and call the methods. It works, but it's not ideal and, while not exactly anti-pattern inheritance, certainly has some code-smell to it.
You should make ValidationView as protocol instead of class, and conform your custom classes to that protocol.
ValidatableView.swift
import UIKit
fileprivate var requiredColor = UIColor.red
fileprivate var requiredText = "*"
fileprivate var requiredFont = UIFont.systemFont(ofSize: 16.0, weight: UIFont.Weight.bold)
fileprivate struct AssociatedKeys {
static var lblKey = "_lblKey_"
}
protocol ValidatableView: class {
var required: Bool {get}
var validRegex: String? {get}
var requiredLbl: UILabel? {get}
}
extension ValidatableView where Self: UIView {
var required: Bool {
return false
}
var validRegex: String? {
return nil
}
var requiredLbl: UILabel? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.lblKey) as? UILabel
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.lblKey, newValue, .OBJC_ASSOCIATION_RETAIN)
}
}
func setupValidation() {
self.requiredLbl = UILabel(frame: CGRect(x: self.frame.width - 30, y: 30, width: 20, height: 20))
self.requiredLbl?.autoresizingMask = .flexibleWidth
self.styleRequiredLabel()
self.addSubview(self.requiredLbl!)
}
func styleRequiredLabel(color:UIColor? = requiredColor, text:String? = requiredText, font:UIFont? = requiredFont) {
self.requiredLbl?.textColor = requiredColor
self.requiredLbl?.text = requiredText
self.requiredLbl?.font = requiredFont
}
}
CustomTextField.swift
#IBDesignable open class CustomTextField: UITextField {
#IBInspectable public var borderWidth: CGFloat = 2.0 {
didSet {
layer.borderWidth = borderWidth
}
}
#IBInspectable public var borderColor: UIColor = UIColor.lightGray {
didSet {
layer.borderColor = borderColor.cgColor
}
}
#IBInspectable public var cornerRadius: CGFloat = 4.0 {
didSet {
layer.cornerRadius = cornerRadius
layer.masksToBounds = true
}
}
public override init(frame: CGRect) {
super.init(frame: frame)
setupValidation()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupValidation()
}
}
extension CustomTextField: ValidatableView { //<- Magic line :)
}
You can create an instance of ValidationView when you instantiate CustomTextField.
Something like this:
#IBDesignable open class CustomTextField: UITextField {
var validationView: ValidationView
override init(frame: CGRect) {
super.init(frame: .zero)
self.validationView = ValidationView()
}
}
Related
I'm trying to add customView to the MDCTabBarItem using mdc_customView but the items are not taking correct width and the results is as below
if I don't set the mdc_customView value then the result is as expected but without the custom design
Code with mdc_customView
override func parseTabBarItems(data: [SubCategory]) -> [MDCTabBarItem] {
var result: [MDCTabBarItem] = []
var nextX: CGFloat = 15
for cat in data {
guard let count = cat.sub?.count, count > 0 else { continue }
let item = MDCTabBarItem()
item.tag = result.count
let customeView = MDCTabBarCustomView()
customeView.frame = CGRect(x: nextX, y: 0, width: (cat.ref ?? "").sizeOfString(usingFont: .ttrSemiBold10).width, height: 50)
nextX = nextX + 15 + (cat.ref ?? "").sizeOfString(usingFont: .ttrSemiBold10).width
customeView.config(title: cat.ref ?? "")
item.mdc_customView = customeView
result.append(item)
}
return result
}
Code without mdc_customView
override func parseTabBarItems(data: [SubCategory]) -> [MDCTabBarItem] {
var result: [MDCTabBarItem] = []
var nextX: CGFloat = 15
for cat in data {
guard let count = cat.sub?.count, count > 0 else { continue }
let item = MDCTabBarItem(title: cat.ref ?? "", image: nil, tag: result.count)
result.append(item)
}
return result
}
MDCTabBarCustomView
import UIKit
import MaterialComponents.MDCTabBarView
class MDCTabBarCustomView: UIView , MDCTabBarViewCustomViewable {
var titleLabel: UILabel!
var containerView: UIView!
var contentFrame: CGRect
init() {
self.titleLabel = UILabel.newAutoLayout()
self.containerView = TTRView.newAutoLayout()
self.contentFrame = .zero
super.init(frame: .zero)
self.autoresizingMask = []
self.setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func config(title: String) {
self.titleLabel.text = title
}
func setSelected(_ selected: Bool, animated: Bool) {}
private func setup(){
self.addSubview(self.containerView)
self.containerView.addSubview(self.titleLabel)
self.containerView.snp.makeConstraints{
$0.edges.equalToSuperview()
}
self.titleLabel.snp.makeConstraints{
$0.edges.equalToSuperview().offset(5)
}
}
}
The tabBar settings:
self.tabBar.preferredLayoutStyle = .scrollable
after spending all the day trying and learning about this new customView I was able to make it work below is the working code
it was all about the intrinsicContentSize and layoutSubviews
here is the new output
final class MDCTabBarCustomView: UIView , MDCTabBarViewCustomViewable {
var contentFrame: CGRect {
return self.titleLabel.frame
}
var titleLabel: UILabel!
var containerView: UIView!
init(){
self.titleLabel = UILabel.newAutoLayout()
self.containerView = UIView.newAutoLayout()
super.init(frame: .zero)
self.autoresizingMask = []
}
override func layoutSubviews() {
super.layoutSubviews()
if self.containerView.superview != self {
self.addSubview(self.containerView)
}
if self.titleLabel.superview != self.containerView {
self.containerView.addSubview(self.titleLabel)
}
containerView.snp.makeConstraints{
$0.top.leading.equalToSuperview().offset(5)
$0.bottom.trailing.equalToSuperview().offset(-5)
}
titleLabel.snp.makeConstraints{
$0.top.equalToSuperview().offset(5)
$0.bottom.equalToSuperview().offset(-5)
$0.centerX.equalToSuperview()
}
}
override var intrinsicContentSize: CGSize {
return CGSize(width: self.titleLabel.intrinsicContentSize.width + 20, height: self.titleLabel.intrinsicContentSize.height + 20)
}
}
Try preferredLayoutStyle = .scrollableCentered
I'm writing a ChartView using NSView with data obtained from a rest api using Combine. The struct PlotView is the SwiftUI View that displays the chart, ChartViewRepresentable is the bridge between the NSView with the chart and the SwiftUI world and ChartView is the view that I actually draw on.
RestRequest gets the data from the network correctly and PlotView has access to it with no issues. When the data is received a ChartViewRepresentable is created and it contains the data, and ChartViewRepresentable creates a ChartView with the data and the data is stored in its data property correctly.
There are two problems: 1) the view's draw method never gets called when the data is loaded, and 2) if the view is redrawn a new ChartViewRepresentable (with a new ChartView) is created by SwiftUI but with no data.
I have connected the RestRequest #StateObject in every possible way imaginable, using #Binding, using #State, with no luck so far, so I'm discounting it as the problem, but with SwiftUI who really knows. It doesn't matter how I load the data, even loading the data manually into ChartView, it never calls the draw method on its own when receiving the data, and then when I for example resize the window to force a draw call it does call the draw method but on a new ChartViewRepresentable struct with no data in it.
What am I doing wrong? This is all the code besides the RestRequest() struct which I know works because I have been using it reliably on other views until now. Any clue or even a hint would be greatly appreciated.
struct PlotView: View {
#StateObject var request = RestRequest()
var body: some View {
Group {
ChartViewRepresentable(data: ChartData(array: ChartData.createArray(from: request.response.data)))
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
}
.onAppear{
let params: [String: String] = [
"limit": "10",
]
request.perform(endPoint: "http://localhost:4000/api/testdata", parameters: params)
}
}
}
struct ChartViewRepresentable: NSViewRepresentable {
typealias NSViewType = ChartView
var chart: ChartView
init(data: ChartData) {
chart = ChartView(data: data)
}
func makeNSView(context: Context) -> ChartView {
return chart
}
func updateNSView(_ nsView: ChartView, context: Context) {
}
}
class ChartView: NSView {
private var data: ChartData
init(data: ChartData) {
self.data = data
print("\(data)")
super.init(frame: .zero)
wantsLayer = true
layer?.backgroundColor = .white
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ dirtyRect: NSRect) {
print("draw call - Frame: \(self.frame), Data: \(data.array.count)")
super.draw(dirtyRect)
guard let context = NSGraphicsContext.current else { return }
context.saveGraphicsState()
if data.array.count > 0 {
//detect data present on ChartView
let ctx = context.cgContext
ctx.setFillColor(NSColor.green.cgColor)
ctx.fillEllipse(in: CGRect(x: 10, y: 10, width: 10, height: 10))
}
context.restoreGraphicsState()
}
}
Right now, in your ChartViewRepresentable, you set the data in init, and then never touch it again.
This means that your ChartView will have its data set before your onAppear API call ever runs and returns data.
To fix this, you'll need to make use of the updateNSView function.
struct ChartViewRepresentable: NSViewRepresentable {
typealias NSViewType = ChartView
var data: ChartData //store the data -- not the chart view
func makeNSView(context: Context) -> ChartView {
return ChartView(data: data)
}
func updateNSView(_ chart: ChartView, context: Context) {
chart.data = data //update the chart view's data
}
}
And, you'll need to respond to the update of that data and force a redraw:
class ChartView: NSView {
var data: ChartData {
didSet {
self.needsDisplay = true //<-- Here
}
}
init(data: ChartData) {
self.data = data
print("\(data)")
super.init(frame: .zero)
wantsLayer = true
layer?.backgroundColor = .white
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ dirtyRect: NSRect) {
print("draw call - Frame: \(self.frame), Data: \(data.array.count)")
super.draw(dirtyRect)
guard let context = NSGraphicsContext.current else { return }
context.saveGraphicsState()
if data.array.count > 0 {
//detect data present on ChartView
let ctx = context.cgContext
ctx.setFillColor(NSColor.green.cgColor)
ctx.fillEllipse(in: CGRect(x: 10, y: 10, width: 10, height: 10))
}
context.restoreGraphicsState()
}
}
Full working example with the API call mocked:
struct ChartData {
var array : [Int]
}
struct ContentView: View {
#State var chartData : ChartData = ChartData(array: [])
var body: some View {
Group {
ChartViewRepresentable(data: chartData)
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
}
.onAppear{
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
print("New data!")
chartData = ChartData(array: [1,2,3,4])
}
}
}
}
struct ChartViewRepresentable: NSViewRepresentable {
typealias NSViewType = ChartView
var data: ChartData
func makeNSView(context: Context) -> ChartView {
return ChartView(data: data)
}
func updateNSView(_ chart: ChartView, context: Context) {
chart.data = data
}
}
class ChartView: NSView {
var data: ChartData {
didSet {
self.needsDisplay = true
}
}
init(data: ChartData) {
self.data = data
print("\(data)")
super.init(frame: .zero)
wantsLayer = true
layer?.backgroundColor = .white
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ dirtyRect: NSRect) {
print("draw call - Frame: \(self.frame), Data: \(data.array.count)")
super.draw(dirtyRect)
guard let context = NSGraphicsContext.current else { return }
context.saveGraphicsState()
if data.array.count > 0 {
//detect data present on ChartView
let ctx = context.cgContext
ctx.setFillColor(NSColor.green.cgColor)
ctx.fillEllipse(in: CGRect(x: 10, y: 10, width: 10, height: 10))
}
context.restoreGraphicsState()
}
}
In my application, I am trying to recreate a live preview picture cropper like in the popular application "Photomath".
Initially, I divided my code into three different classes. The Live preview view (a UIView), an Overlay View (a UIView also), and the actual Cropper View.
Currently, my thought process is to add the CropperView into the Overlay and then the Overlay into the Live Preview View. So far that's working.
I have a UIPanGesture hooked up to the CropperView in order to have it move around eventually. However, here is where the problem resides.
The inside of the cropper is clear or less dark than the surrounding overlay. I've heard that you can achieve this using a mask but have not been successful.
Here is the code for the Live Preview View
class NotationCameraView : UIView {
private var session : AVCaptureSession!
private var stillImageOutput : AVCapturePhotoOutput!
private var videoPreviewLayer : AVCaptureVideoPreviewLayer!
private var previewCropper : OverlayCropper!
override init(frame: CGRect) {
super.init(frame: frame)
self.commonInitializer()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.commonInitializer()
}
private func commonInitializer() {
if AVCaptureDevice.authorizationStatus(for: .video) == .authorized {
self.setUpLivePreview()
self.setUpOverlayCropper()
print("Set up view")
} else {
AVCaptureDevice.requestAccess(for: .video) { (success) in
if success {
self.setUpLivePreview()
self.setUpOverlayCropper()
} else {
print("Give me your camera REEEEEEEEEEE")
}
}
}
}
/**
Sets up the live preview of the back camera
*/
private func setUpLivePreview() {
self.session = AVCaptureSession()
self.session.sessionPreset = .high
let backCamera = AVCaptureDevice.default(for: AVMediaType.video)
//MARK: Need to have an overlay view so that the user does not see the video preview frame being upscaled
do {
let input = try AVCaptureDeviceInput(device: backCamera!)
self.stillImageOutput = AVCapturePhotoOutput()
if self.session.canAddInput(input) && self.session.canAddOutput(self.stillImageOutput) {
self.session.addInput(input)
self.session.addOutput(self.stillImageOutput)
self.videoPreviewLayer = AVCaptureVideoPreviewLayer(session: self.session)
self.videoPreviewLayer.videoGravity = .resizeAspectFill
self.videoPreviewLayer.connection?.videoOrientation = .portrait
self.layer.addSublayer(videoPreviewLayer)
//Have public methods for starting or stopping the capture session
DispatchQueue.global(qos: .userInitiated).async {
self.session.startRunning()
DispatchQueue.main.async {
self.videoPreviewLayer.frame = self.bounds
}
}
}
} catch {
print("Need to allow permission to the back camera")
}
}
/**
Sets up the overlay cropper for the live preview.
*/
private func setUpOverlayCropper() {
self.previewCropper = OverlayCropper(frame: self.bounds)
self.previewCropper.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(self.previewCropper)
}
}
Here is the code for the Overlay.
class OverlayCropper : UIView {
private var cropperView : CropperView!
override init(frame: CGRect) {
super.init(frame: frame)
self.commonInitializer()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.commonInitializer()
}
private func commonInitializer() {
self.cropperView = CropperView(frame: CGRect(x: 50, y: 100, width: 100, height: 100))
self.cropperView.backgroundColor = UIColor.blue
self.cropperView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(cropperView)
self.backgroundColor = UIColor.black.withAlphaComponent(0.25)
}
}
and lastly here is the code for the CropperView
class CropperView : UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.commonInitializer()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.commonInitializer()
}
private func commonInitializer() {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.handlePanGesture(_:)))
self.addGestureRecognizer(panGesture)
self.makeMask()
}
#objc private func handlePanGesture(_ panGesture : UIPanGestureRecognizer) {
let translation = panGesture.translation(in: self)
//print(translation.x, translation.y)
self.center = CGPoint(x: self.center.x + translation.x, y: self.center.y + translation.y)
panGesture.setTranslation(CGPoint.zero, in: self)
}
private func makeMask() {
let path = CGMutablePath()
path.addRect(CGRect(origin: .zero, size: self.frame.size))
let maskLayer = CAShapeLayer()
maskLayer.backgroundColor = UIColor.clear.cgColor
maskLayer.path = path
maskLayer.fillRule = .evenOdd
self.layer.mask = maskLayer
self.clipsToBounds = true
}
}
I have created Cocoa Touch Class:
Sub Class Of UITextField
And the file Name is MyTextFieldStyle
And here is the MyTextFieldStyle.swift file:
import UIKit
#IBDesignable
class MyTextFieldStyle: UITextField {
#IBInspectable var FavoriteTextColor : UIColor = UIColor.white {
didSet {
self.textColor = FavoriteTextColor
}
}
override func awakeFromNib() {
super.awakeFromNib()
self.textColor = self.FavoriteTextColor
}
}
How can I add these 3 controls,
placeHolder text color
place holder font type
place holder font size
like 'FavoriteTextColor' above ? (Swift 4.2 & Xcode 10)
I'd do my IBDesignables differently. I would put all my final assignment in the same function for an example here is my own custom UITextField I use.
#IBDesignable public class HooopTextfield: UITextField, UITextFieldDelegate {
#IBInspectable public var fontName: String? = "AvenirNext-Bold" {
didSet {
decorate()
}
}
#IBInspectable public var fontSize: CGFloat = 15 {
didSet {
decorate()
}
}
#IBInspectable public var fontColor: UIColor = UIColor.white {
didSet {
decorate()
}
}
#IBInspectable public var customTextAlignment: Int = 0 {
didSet {
decorate()
}
}
#IBInspectable public var borderColor: UIColor = UIColor.white {
didSet {
decorate()
}
}
#IBInspectable public var letterSpacing: CGFloat = 0 {
didSet {
decorate()
}
}
#IBInspectable public var cornerRadius: CGFloat = 0 {
didSet {
decorate()
}
}
#IBInspectable public var customPlaceholder: String? = nil {
didSet {
decorate()
}
}
#IBInspectable public var horizontalInset: CGFloat = 0 {
didSet {
decorate()
}
}
#IBInspectable public var verticalInset: CGFloat = 0 {
didSet {
decorate()
}
}
#IBInspectable public var selfDelegate: Bool = false {
didSet {
if selfDelegate {
self.delegate = self
}
}
}
#IBInspectable public var borderWidth: CGFloat = 0 {
didSet {
decorate()
}
}
#IBInspectable public var baseLineOffset: CGFloat = 0 {
didSet {
decorate()
}
}
#IBInspectable public var placeholderColor: UIColor? = nil {
didSet {
decorate()
}
}
#IBInspectable public var requiredColor: UIColor? = nil {
didSet {
decorate()
}
}
#IBInspectable public var requiredCharacter: String = "*"{
didSet {
decorate()
}
}
#IBOutlet public var nextField:HooopTextfield?
/*** more inspectable var can be added **/
override public func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: horizontalInset, dy: verticalInset)
}
override public func editingRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: horizontalInset, dy: verticalInset)
}
func decorate() {
// Setup border and corner radius
self.layer.cornerRadius = cornerRadius
self.layer.borderWidth = borderWidth
self.layer.borderColor = borderColor.cgColor
// Setup text style
let paragraphStyle: NSMutableParagraphStyle = NSMutableParagraphStyle()
switch customTextAlignment {
case 2:
paragraphStyle.alignment = .right
break
case 1:
paragraphStyle.alignment = .center
break
default:
paragraphStyle.alignment = .left
break
}
var titleAttributes:[NSAttributedStringKey : Any] = [
NSAttributedStringKey.foregroundColor: fontColor,
NSAttributedStringKey.kern: letterSpacing,
NSAttributedStringKey.baselineOffset: baseLineOffset,
NSAttributedStringKey.paragraphStyle: paragraphStyle
]
if let _ = fontName {
titleAttributes[NSAttributedStringKey.font] = UIFont(name: fontName!, size: fontSize)
}
if let _ = customPlaceholder {
var placeholderAttributes = titleAttributes
if let _ = placeholderColor {
placeholderAttributes[NSAttributedStringKey.foregroundColor] = placeholderColor
}
let attributedPlaceholder = NSMutableAttributedString(string: customPlaceholder!, attributes: placeholderAttributes)
if let _ = requiredColor {
let range = (customPlaceholder! as NSString).range(of: requiredCharacter)
attributedPlaceholder.addAttribute(NSAttributedStringKey.foregroundColor, value: requiredColor!, range: range)
}
self.attributedPlaceholder = attributedPlaceholder
}
self.defaultTextAttributes = titleAttributes
}
// MARK: - UITexfieldDelegate
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if (nextField != nil) {
nextField?.becomeFirstResponder()
}
return true
}
}
I have basic stuff like fontColor, fontSize, fontName, same things for the placeholder. Then I have more visual things like borderColor, cornerRadius, borderWidth, etc. Finally I also have some inset rect positioning to align everything as I wish right from my storyboard.
I use NSMutableAttributedString for these because they are the most customisable, allowing me to also be able to color required fields and such.
Finally I also have a little #IBOutlet that also allows me to jump from one textfield to the next with the next button on the keyboard when combined with a boolean called selfDelegate.
Ask me any questions you want about this and play around with it you should be able to do anything you want with #IBDesignable I think.
EDIT 1:
Before I forget I recommend using a decorate or equivalent function because the order you apply your changes matter most of the time.
EDIT 2:
Fixed the errors, this was due most likely to swift changing how to use NSAttributedString attributes but strangely enough it seems the placeholderAttributes needed to be [NSAttributedStringKey: Any] but the defaultTextAttributes needed to be [String: Any] don't know why this would be the case. But it should work now.
EDIT 3:
Probably because I have a different version of Xcode or because it was a playground I had a message for defaultTextAttributes but I've refixed it by removing the .rawValue if it was the opposite that needed to be done, know that NSAttributedStringKey.key.rawValue will be a string and you can get the NSAttributedStringKey by using NSAttributedStringKey.init(rawValue:String)
I'm missing something here. I got Liveness to work in Interface Builder. I can draw the image in the NSView with NSImage.draw rect. So the image loads correctly. However when I put this inside a CALayer it doesn't show up.
Did I miss something about behaviour on NSView? CALayer? Layer Hosting? Or something else?
Here's the code of the view:
import Foundation
import AppKit
import QuartzCore
#IBDesignable public class CircularImageView: NSView {
var imageLayer: CALayer?
#IBInspectable public var edgeInset: CGFloat = 10
public var image: NSImage? {
didSet {
if let newImage = image {
imageLayer?.contents = newImage
}
}
}
// MARK: New in this class
private func prepareLayer() {
self.layer = CALayer()
self.wantsLayer = true
}
private func drawImage() {
// What am I doing wrong here?
var newImageLayer = CALayer()
newImageLayer.contentsGravity = kCAGravityResizeAspect
if let imageToSet = image {
newImageLayer.contents = imageToSet
}
let insetBounds = CGRectInset(self.bounds, edgeInset, edgeInset)
newImageLayer.frame = insetBounds
newImageLayer.backgroundColor = NSColor(calibratedWhite: 0.8, alpha: 1).CGColor
self.layer!.addSublayer(newImageLayer)
imageLayer = newImageLayer
}
private func test(){
image?.drawInRect(self.bounds)
}
// MARK: NSView stuff
public override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
prepareLayer()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
prepareLayer()
}
public override func viewWillDraw() {
super.viewWillDraw()
drawImage()
}
public override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
// Load default test image.
println("\(self): prepareForInterfaceBuilder")
let processInfo = NSProcessInfo.processInfo()
let environment = processInfo.environment
let projectSourceDirectories : AnyObject = environment["IB_PROJECT_SOURCE_DIRECTORIES"]!
let directories = projectSourceDirectories.componentsSeparatedByString(":")
if directories.count != 0 {
let firstPath = directories[0] as! String
let imagePath = firstPath.stringByAppendingPathComponent("CircularView/Bz1dSvR.jpg")
let image = NSImage(contentsOfFile: imagePath)
image!.setName("Test Image")
self.image = image
}
}
}
Thanks in advance.
Okay, guys I found the answer.
I did two things wrong in this code. I was working on a layer hosting view and I simply needed a layer-backed view. I didn't new there was a difference. And before adding an NSImage to the CALayer's contents I needed to embrace it with lockFocus() and unlockFocus().
Here's the full code that solved the issue.
import Foundation
import AppKit
#IBDesignable public class CircularImageView: NSView {
var imageLayer: CALayer?
#IBInspectable public var edgeInset: CGFloat = 10
public var image: NSImage? {
didSet {
if let newImage = image {
imageLayer?.contents = newImage
}
}
}
// MARK: New in this class
private func prepareLayer() {
// I had to remove my own created layer.
self.wantsLayer = true
}
private func drawImage() {
var newImageLayer = CALayer()
newImageLayer.contentsGravity = kCAGravityResizeAspect
if let imageToSet = image {
// I didn't lock the focus on the imageToSet.
imageToSet.lockFocus()
newImageLayer.contents = imageToSet
// I didn't unlock the focus either.
imageToSet.unlockFocus()
}
let insetBounds = CGRectInset(self.bounds, edgeInset, edgeInset)
newImageLayer.frame = insetBounds
newImageLayer.backgroundColor = NSColor(calibratedWhite: 0.8, alpha: 1).CGColor
self.layer!.addSublayer(newImageLayer)
imageLayer = newImageLayer
}
// MARK: NSView stuff
public override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
prepareLayer()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
prepareLayer()
}
public override func viewWillDraw() {
super.viewWillDraw()
drawImage()
}
public override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
// Load default test image.
println("\(self): prepareForInterfaceBuilder")
let processInfo = NSProcessInfo.processInfo()
let environment = processInfo.environment
let projectSourceDirectories : AnyObject = environment["IB_PROJECT_SOURCE_DIRECTORIES"]!
let directories = projectSourceDirectories.componentsSeparatedByString(":")
if directories.count != 0 {
let firstPath = directories[0] as! String
let imagePath = firstPath.stringByAppendingPathComponent("CircularView/Bz1dSvR.jpg")
let image = NSImage(contentsOfFile: imagePath)
image!.setName("Test Image")
self.image = image
}
}
}