如何在 Xcode 中创建自定义 UIView?

Posted

技术标签:

【中文标题】如何在 Xcode 中创建自定义 UIView?【英文标题】:How do I create a custom UIView in Xcode? 【发布时间】:2018-06-18 06:41:03 【问题描述】:

我正在尝试使用 UIView 上的一些 UISegementedControls 创建一个相对简单的自定义视图(见下文),但我似乎无法让事情正常工作。

我为视图创建 XIB 文件并将文件的所有者设置为自定义类(见下文)。然后我通过 CTRL-Dragging 直接从右键单击文件所有者时弹出的面板链接@IBOutlets。

在代码中,我还为 UISegmentedControls 设置了目标,但这些目标似乎永远不会被调用,同样,如果我设置 isSelectionEnabled,分段控件永远不会在视图中被禁用。

我在这里做错了什么?

//
//  ScheduleSelectorView.swift
//  
//
//  .
//  
//

import UIKit

enum ScheduleWeeks: String 
    case week1 = "Week 1",
    week2 = "Week 2",
    week3 = "week 3",
    week4 = "week 4"

    static let allValues = [week1,week2,week3,week4]

enum ScheduleDays: String 
    case mon = "Mon",
    tue = "Tue",
    wed = "Wed",
    thu = "Thu",
    fri = "Fri",
    sat = "Sat",
    sun = "Sun"

    static let allValues = [mon,tue,wed,thu,fri,sat,sun]

enum ScheduleAMPM: String 
    case am = "AM",
    pm = "PM"

    static let allValues = [am, pm]

@objc
enum ScheduleSelectorValueType: Int 
    case week, day, hour, minute, ampm, none


@objc
protocol ScheduleSelectorDelegate 

    /// called when a value is changed in the selector
    @objc
    func valueChanged(type: ScheduleSelectorValueType, value: Any?)


@IBDesignable class ScheduleSelectorView: UIView 

    let nibName = "ScheduleSelectorView"

    @IBOutlet var delegate: ScheduleSelectorDelegate?

    @IBOutlet var contentView: UIView!

    @IBOutlet weak var weekSelector: UISegmentedControl!
    @IBOutlet weak var daySelector: UISegmentedControl!
    @IBOutlet weak var hourSelector: UISegmentedControl!
    @IBOutlet weak var minuteSelector: UISegmentedControl!
    @IBOutlet weak var ampmSelector: UISegmentedControl!

    var selectors: [UISegmentedControl] = []


    @IBInspectable
    var timeInterval: Int = 15 
        didSet 
            setupMinuteSelector()
        
    

    var isSelectionEnabled: Bool = false 
        didSet 
            for selector in selectors 
                DebugLog("isSelectionEnabled: \(isSelectionEnabled): \(selector.numberOfSegments)")
                selector.isUserInteractionEnabled = isSelectionEnabled
            
        
    
    struct Schedule 
        var week:ScheduleWeeks = .week1
        var day: ScheduleDays = .mon
        var hour: Int = 0
        var minute: Int = 0
        var ampm: ScheduleAMPM = .am

        init() 

        
        init(week: ScheduleWeeks, day: ScheduleDays, hour: Int, minute: Int, ampm: ScheduleAMPM) 
            self.week = week
            self.day = day
            self.hour = hour
            self.minute = minute
            self.ampm = ampm
        
    

    var _schedule = Schedule()

    var schedule: Schedule? 
        return _schedule
    

    required init?(coder aDecoder: NSCoder) 
        super.init(coder: aDecoder)
        commonInit()
        DebugLog("init(coder)")
    

    override init(frame: CGRect) 
        super.init(frame: frame)
        commonInit()
        DebugLog("init(frame)")
    

    func loadViewFromNib() -> UIView 
        // grabs the appropriate bundle
        let bundle = Bundle(for: type(of: self))
        let nib = UINib(nibName: nibName, bundle: bundle)
        let view = nib.instantiate(withOwner: self, options: nil)[0] as! UIView
        return view
    
    func commonInit() 
        Bundle.main.loadNibNamed(nibName, owner: self, options: nil)
        addSubview(contentView)
        contentView.frame = self.bounds
        contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        selectors = [weekSelector, daySelector, hourSelector, minuteSelector, ampmSelector]
        setup()
    

    func setup()
        setupWeekSelector()
        setupDaySelector()
        setupHourSelector()
        setupMinuteSelector()

        // Set targets for changes
        self.weekSelector.addTarget(self, action: #selector(selectionDidChange(_:)), for: .valueChanged )
        self.daySelector.addTarget(self, action: #selector(selectionDidChange(_:)), for: .valueChanged )
        self.hourSelector.addTarget(self, action: #selector(selectionDidChange(_:)), for: .valueChanged )
        self.minuteSelector.addTarget(self, action: #selector(selectionDidChange(_:)), for: .valueChanged )
        self.ampmSelector.addTarget(self, action: #selector(selectionDidChange(_:)), for: .valueChanged )
    
    func setupWeekSelector()
        DebugLog("setupWeekSelector")
        weekSelector.removeAllSegments()

        for (index, title) in ScheduleWeeks.allValues.enumerated() 
            weekSelector.insertSegment(withTitle: title.rawValue, at: index, animated: true)
        
    
    func setupDaySelector()
        daySelector.removeAllSegments()

        for (index, title) in ScheduleDays.allValues.enumerated() 
            daySelector.insertSegment(withTitle: title.rawValue, at: index, animated: true)
        
    
    func setupHourSelector()
        hourSelector.removeAllSegments()

        for t in 0..<12 
            hourSelector.insertSegment(withTitle: "\(t)", at: t, animated: true)
        
    
    func setupMinuteSelector()
        minuteSelector?.removeAllSegments()

        for t in stride(from: 0, through: 60, by: timeInterval) 
            minuteSelector?.insertSegment(withTitle: "\(t)", at: t, animated: true)
        
    
    @objc func selectionDidChange(_ selector: UISegmentedControl) 

        var type: ScheduleSelectorValueType = .none
        var value: Any? = nil
        let index = selector.selectedSegmentIndex

        switch selector 
        case weekSelector:
            type = .week
            value = ScheduleWeeks.allValues[index]
            _schedule.week = ScheduleWeeks.allValues[index]
        case daySelector:
            type = .day
            value = ScheduleDays.allValues[index]
            _schedule.day = ScheduleDays.allValues[index]
        case hourSelector:
            type = .hour
            value = index
            _schedule.hour = index
        case minuteSelector:
            type = .minute
            value = index
            _schedule.minute = index
        case ampmSelector:
            type = .ampm
            value = ScheduleAMPM.allValues[index]
            _schedule.ampm = ScheduleAMPM.allValues[index]
        default:
            type = .none
        

        self.delegate?.valueChanged(type: type, value: value)
    
    var valueString: String 
        let week = (weekSelector.selectedSegmentIndex != UISegmentedControlNoSegment) ? weekSelector.selectedSegmentIndex : 0
        let day = (daySelector.selectedSegmentIndex != UISegmentedControlNoSegment) ? daySelector.selectedSegmentIndex : 0
        let hour = (hourSelector.selectedSegmentIndex != UISegmentedControlNoSegment) ? hourSelector.selectedSegmentIndex : 0
        let minute = (minuteSelector.selectedSegmentIndex != UISegmentedControlNoSegment) ? minuteSelector.selectedSegmentIndex : 0
        let ampm = (ampmSelector.selectedSegmentIndex != UISegmentedControlNoSegment) ? ampmSelector.selectedSegmentIndex : 0
        let selection = "\(ScheduleWeeks.allValues[week]), \(ScheduleDays.allValues[day]) \(hour.padded2):\(minute.padded2) \(ScheduleAMPM.allValues[ampm])"
        return selection
    
    /*
    // MARK: - Navigation

    // In a storyboard-based application, you will often want to do a little preparation before navigation
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) 
        // Get the new view controller using segue.destinationViewController.
        // Pass the selected object to the new view controller.
    
    */



extension UISegmentedControl 

    func setSelectionEnabled(_ enabled: Bool) 

            for i in 0..<self.numberOfSegments 
                self.setEnabled(enabled, forSegmentAt: i)
        

    


【问题讨论】:

【参考方案1】:

我真的很难找到任何真正有效的答案,但最终我设法让它发挥作用。

我认为主要的解决方案来自我在这里找到的 NibLoaderView。

ios - How to initialize custom UIView with specific Frame from NIB

但我必须进行一些更改才能使其在 IB 中工作。

import UIKit

// Usage: Subclass your UIView from NibLoadView to automatically load a xib with the same name as your class

@IBDesignable
class NibLoadingView: UIView 

    @IBOutlet weak var view: UIView!

    override init(frame: CGRect) 
        super.init(frame: frame)
        nibSetup()
    

    required init?(coder aDecoder: NSCoder) 
        super.init(coder: aDecoder)
        nibSetup()
    
    override open func prepareForInterfaceBuilder() 
        super.prepareForInterfaceBuilder()
        nibSetup()
        view?.prepareForInterfaceBuilder()
    

    private func nibSetup() 
        backgroundColor = .clear

        view = loadViewFromNib()
        view.frame = bounds
        view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.translatesAutoresizingMaskIntoConstraints = true

        addSubview(view)
        view.anchorAllEdgesToSuperview()
    

    private func loadViewFromNib() -> UIView 
        let bundle = Bundle(for: type(of:self))
        let nib = UINib(nibName: String(describing: type(of:self)), bundle: bundle)
        let nibView = nib.instantiate(withOwner: self, options: nil).first as! UIView

        return nibView
    



extension UIView 
    func anchorAllEdgesToSuperview() 
        self.translatesAutoresizingMaskIntoConstraints = false
        if #available(iOS 9.0, *) 
            addSuperviewConstraint(constraint: topAnchor.constraint(equalTo: (superview?.topAnchor)!))
            addSuperviewConstraint(constraint: leftAnchor.constraint(equalTo: (superview?.leftAnchor)!))
            addSuperviewConstraint(constraint: bottomAnchor.constraint(equalTo: (superview?.bottomAnchor)!))
            addSuperviewConstraint(constraint: rightAnchor.constraint(equalTo: (superview?.rightAnchor)!))
        
        else 
            for attribute : NSLayoutAttribute in [.left, .top, .right, .bottom] 
                anchorToSuperview(attribute: attribute)
            
        
    

    func anchorToSuperview(attribute: NSLayoutAttribute) 
        addSuperviewConstraint(constraint: NSLayoutConstraint(item: self, attribute: attribute, relatedBy: .equal, toItem: superview, attribute: attribute, multiplier: 1.0, constant: 0.0))
    

    func addSuperviewConstraint(constraint: NSLayoutConstraint) 
        superview?.addConstraint(constraint)
    

然后在子类中确保您拥有以下内容:

@IBDesignable class Designable: NibLoaderView 

    override init(frame: CGRect) 
        super.init(frame: frame)
    

    required init?(coder aDecoder: NSCoder) 
        super.init(coder: aDecoder)
    


我当然希望这可以节省其他人一些时间......

如果有人感兴趣,这里有一个工作示例应用程序

https://ossh.com.au/design-and-technology/software-development/xcode-9-swift-4-ibdesignables-with-xib/

【讨论】:

一个优雅的解决方案。但我认为 nibSetup() 中有几行多余的行。 view.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.translatesAutoresizingMaskIntoConstraints = true 不是必需的,因为无论如何您都可以在 anchorAllEdgesToSuperview() 中撤消此操作。此外,子类中显示的 init() 方法也不是必需的,它们什么都不做。不幸的是,层次结构中还有一个额外的 UIView,但它仍然比我能找到的任何其他解决方案更好地解决了这个问题

以上是关于如何在 Xcode 中创建自定义 UIView?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Swift Xcode 中创建自定义导航栏?

如何在Xcode + Swift 4中创建自定义UIBarButtonItem类?

在 Swift 中创建自定义 UIView 并显示为弹出窗口

如何使用不同的 init 方法在 XIB 中创建自定义视图?

重构大方法以在 UIViewController 中创建自定义 UIView

在 Xcode 中创建自定义 xib 视图大小