스토리 보드의 여러보기 컨트롤러에서보기를 사용하고 싶습니다. 따라서 외부 xib에서 뷰를 디자인하여 변경 사항이 모든 뷰 컨트롤러에 반영되도록 생각했습니다. 그러나 스토리 보드의 외부 xib에서 뷰를 어떻게로드 할 수 있습니까? 그렇지 않은 경우 상황에 맞도록 다른 대안이 있습니까?
스토리 보드의 여러보기 컨트롤러에서보기를 사용하고 싶습니다. 따라서 외부 xib에서 뷰를 디자인하여 변경 사항이 모든 뷰 컨트롤러에 반영되도록 생각했습니다. 그러나 스토리 보드의 외부 xib에서 뷰를 어떻게로드 할 수 있습니까? 그렇지 않은 경우 상황에 맞도록 다른 대안이 있습니까?
답변:
내 전체 예는 여기 있지만 아래에 요약을 제공합니다.
나열한 것
동일한 이름을 가진 .swift 및 .xib 파일을 프로젝트에 추가하십시오. .xib 파일에는 사용자 정의보기 레이아웃이 포함됩니다 (자동 레이아웃 제약 조건을 사용하는 것이 좋습니다).
빠른 파일을 xib 파일의 소유자로 만듭니다.
다음 코드를 .swift 파일에 추가하고 .xib 파일에서 콘센트와 작업을 연결하십시오.
import UIKit
class ResuableCustomView: UIView {
let nibName = "ReusableCustomView"
var contentView: UIView?
@IBOutlet weak var label: UILabel!
@IBAction func buttonTap(_ sender: UIButton) {
label.text = "Hi"
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
guard let view = loadViewFromNib() else { return }
view.frame = self.bounds
self.addSubview(view)
contentView = view
}
func loadViewFromNib() -> UIView? {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: nibName, bundle: bundle)
return nib.instantiate(withOwner: self, options: nil).first as? UIView
}
}
그걸 써
스토리 보드의 어느 곳에서나 사용자 정의보기를 사용하십시오. a를 추가 UIView
하고 클래스 이름을 사용자 정의 클래스 이름으로 설정하십시오.
잠시 동안 Christopher Swasey의 접근 방식 은 내가 찾은 최고의 접근 방식 이었습니다. 나는 우리 팀의 두 명의 수석 개발자에게 그것에 대해 물었고 그들 중 하나 는 완벽한 솔루션을 가지고있었습니다 ! 그것은 크리스토퍼 스와 지 (Christopher Swasey)가 웅변 적으로 해결 한 문제를 모두 만족 시키며 보일러 플레이트 하위 클래스 코드 (필요한 접근 방식)가 필요하지 않습니다. 이 하나 잡았다 , 그러나 상당히 직관적이고 쉽게 구현할 수 있다는 이외는.
MyCustomClass.swift
MyCustomClass.xib
File's Owner
.xib 파일은 (사용자 정의 클래스가 될 수 있습니다 MyCustomClass
)class
(세 이하 값 identity Inspector
.xib 파일 빈에서 사용자 정의보기를 들어). 따라서 사용자 정의보기에는 지정된 클래스가 없지만 지정된 파일 소유자가 있습니다.Assistant Editor
.
Connections Inspector
참조 아웃렛이 사용자 정의 클래스 (예 MyCustomClass
:)를 참조하는 것이 아니라 참조라는 것을 알 수 File's Owner
있습니다. 이후 File's Owner
사용자 정의 클래스로 지정되고, 출구는 후크와 일 때에 프로퍼티됩니다.NibLoadable
아래 참조 된 프로토콜을 준수하도록하십시오 .
.swift
파일 이름이 파일 이름과 다른 .xib
경우 nibName
속성을 파일 이름으로 설정 .xib
하십시오.required init?(coder aDecoder: NSCoder)
하고 override init(frame: CGRect)
호출 setupFromNib()
하십시오.MyCustomClass
.참조하려는 프로토콜은 다음과 같습니다.
public protocol NibLoadable {
static var nibName: String { get }
}
public extension NibLoadable where Self: UIView {
public static var nibName: String {
return String(describing: Self.self) // defaults to the name of the class implementing this protocol.
}
public static var nib: UINib {
let bundle = Bundle(for: Self.self)
return UINib(nibName: Self.nibName, bundle: bundle)
}
func setupFromNib() {
guard let view = Self.nib.instantiate(withOwner: self, options: nil).first as? UIView else { fatalError("Error loading \(self) from nib") }
addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
view.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
view.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
view.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
view.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
}
}
다음은 MyCustomClass
프로토콜을 구현 하는 예제입니다 (.xib 파일 이름 MyCustomClass.xib
).
@IBDesignable
class MyCustomClass: UIView, NibLoadable {
@IBOutlet weak var myLabel: UILabel!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupFromNib()
}
override init(frame: CGRect) {
super.init(frame: frame)
setupFromNib()
}
}
참고 : Gotcha를 놓치고 class
.xib 파일 내부 의 값을 사용자 정의 클래스로 설정하면 스토리 보드에 그려지지 않으며 EXC_BAD_ACCESS
앱이 무한 루프에 빠지기 때문에 앱을 실행할 때 오류가 발생합니다 init?(coder aDecoder: NSCoder)
메소드를 사용하여 펜촉에서 클래스를 초기화하려고 시도한 다음 다시 Self.nib.instantiate
호출 init
합니다.
setupFromNib()
은 XIB 작성 뷰를 포함하는 자동 크기 조정 테이블 뷰 셀의 특정 이상한 자동 레이아웃 문제를 해결하는 것으로 보입니다.
@IBDesignable
호환성을 좋아 합니다. Xcode 또는 UIKit이 UIView 파일을 추가 할 때 기본값으로 이와 같은 것을 제공하지 않는 이유를 알 수 없습니다.
사용하려는 xib를 작성했다고 가정하십시오.
1) UIView의 사용자 정의 서브 클래스를 작성하십시오 (파일-> 새로 작성-> 파일 ...-> Cocoa Touch Class로 이동하십시오. "서브 클래스 :"가 "UIView"인지 확인하십시오).
2) xib를 기반으로하는 뷰를 초기화시이 뷰에 하위 뷰로 추가하십시오.
Obj-C에서
-(id)initWithCoder:(NSCoder *)aDecoder{
if (self = [super initWithCoder:aDecoder]) {
UIView *xibView = [[[NSBundle mainBundle] loadNibNamed:@"YourXIBFilename"
owner:self
options:nil] objectAtIndex:0];
xibView.frame = self.bounds;
xibView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self addSubview: xibView];
}
return self;
}
스위프트 2에서
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let xibView = NSBundle.mainBundle().loadNibNamed("YourXIBFilename", owner: self, options: nil)[0] as! UIView
xibView.frame = self.bounds
xibView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
self.addSubview(xibView)
}
스위프트 3에서
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let xibView = Bundle.main.loadNibNamed("YourXIBFilename", owner: self, options: nil)!.first as! UIView
xibView.frame = self.bounds
xibView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.addSubview(xibView)
}
3) 스토리 보드에서 사용하려는 곳마다 평소와 같이 UIView를 추가하고 새로 추가 된보기를 선택하고 Identity Inspector (오른쪽 상단의 세 번째 아이콘으로 선이있는 사각형 모양)로 이동하십시오. "Custom Class"아래에 "Class"로 서브 클래스의 이름을 입력하십시오.
xibView.frame = self.frame;
되어야 xibView.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
달리 xibView 뷰가 스토리 보드에 추가 될 때 오프셋을 가질 것이다.
나는 항상 (1) 자동 레이아웃, (2) @IBInspectable
및 (3) 콘센트로 나사를 조이는 것을 보면서 "서브 뷰로 추가"솔루션이 만족스럽지 않다는 것을 알게되었습니다 . 대신, 내가의 마법을 소개하자 awakeAfter:
, NSObject
방법.
awakeAfter
NIB / Storyboard에서 실제로 깨어 난 객체를 완전히 다른 객체로 교체 할 수 있습니다. 그런 다음 그 물체는 수화 과정을 거쳐 awakeFromNib
호출되고 뷰로 추가됩니다.
이것을 뷰의 "카드 보드 컷 아웃"서브 클래스에서 사용할 수 있습니다. 유일한 목적은 NIB에서 뷰를로드하고 스토리 보드에서 사용하기 위해 반환하는 것입니다. 그런 다음 포함 가능한 하위 클래스가 원본 클래스가 아닌 스토리 보드보기의 ID 관리자에서 지정됩니다. 이것이 작동하기 위해 실제로 서브 클래스 일 필요는 없지만, 서브 클래스로 만드는 것은 IB가 IBInspectable / IBOutlet 특성을 볼 수있게하는 것입니다.
이 여분의 상용구는 최적이 아닌 것처럼 보일 수 있습니다. 이상적으로 UIStoryboard
는 완벽하게 처리 할 수 있기 때문 입니다. 그러나 원래 NIB 및 UIView
하위 클래스를 완전히 수정하지 않은 상태 로 둘 수 있다는 이점이 있습니다. 그것이 수행하는 역할은 기본적으로 어댑터 또는 브리지 클래스의 역할이며, 유감스럽게도 추가 클래스로서 디자인 측면에서 완벽하게 유효합니다. 반면에, 클래스와의 조화를 원한다면 @BenPatch의 솔루션은 다른 사소한 변경 사항이있는 프로토콜을 구현하여 작동합니다. 어떤 솔루션이 더 나은지에 대한 질문은 프로그래머 구성의 문제로 요약됩니다. 하나는 객체 구성을 선호하는지 아니면 다중 상속을 선호하는지 여부입니다.
참고 : NIB 파일의보기에서 설정된 클래스는 동일하게 유지됩니다. 임베드 가능한 서브 클래스는 스토리 보드 에서만 사용됩니다. 코드에서 뷰를 인스턴스화하는 데 서브 클래스를 사용할 수 없으므로 추가 로직 자체가 없어야합니다. 후크 만 포함 해야 합니다awakeAfter
.
class MyCustomEmbeddableView: MyCustomView {
override func awakeAfter(using aDecoder: NSCoder) -> Any? {
return (UIView.instantiateViewFromNib("MyCustomView") as MyCustomView?)! as Any
}
}
⚠️ 여기서 중요한 단점은 스토리 보드에서 다른 뷰와 관련이없는 너비, 높이 또는 종횡비 제약 조건을 정의하면 수동으로 복사해야한다는 것입니다. 두 개의 뷰와 관련된 제약 조건은 가장 가까운 공통 조상에 설치되며 뷰는 내부에서 스토리 보드에서 깨어 났으므로 이러한 제약 조건이 슈퍼 뷰에서 수화 될 때까지 스왑이 이미 발생했습니다. 해당 뷰에만 관련된 제약 조건은 해당 뷰에 직접 설치되므로 스왑이 발생하지 않으면 복사되지 않습니다.
여기서 일어나는 것은 스토리 보드 의 뷰 에 설치된 제약 조건 이 새로 인스턴스화 된 뷰로 복사되며, 이미 nib 파일에 정의 된 자체 제약 조건이있을 수 있습니다. 그것들은 영향을받지 않습니다.
class MyCustomEmbeddableView: MyCustomView {
override func awakeAfter(using aDecoder: NSCoder) -> Any? {
let newView = (UIView.instantiateViewFromNib("MyCustomView") as MyCustomView?)!
for constraint in constraints {
if constraint.secondItem != nil {
newView.addConstraint(NSLayoutConstraint(item: newView, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: newView, attribute: constraint.secondAttribute, multiplier: constraint.multiplier, constant: constraint.constant))
} else {
newView.addConstraint(NSLayoutConstraint(item: newView, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: constraint.constant))
}
}
return newView as Any
}
}
instantiateViewFromNib
형식 안전 확장 UIView
입니다. 유형과 일치하는 것을 찾을 때까지 NIB의 객체를 반복합니다. 제네릭 형식은 반환 값이므로 호출 사이트에서 형식을 지정해야합니다.
extension UIView {
public class func instantiateViewFromNib<T>(_ nibName: String, inBundle bundle: Bundle = Bundle.main) -> T? {
if let objects = bundle.loadNibNamed(nibName, owner: nil) {
for object in objects {
if let object = object as? T {
return object
}
}
}
return nil
}
}
instantiateViewFromNib
아무것도 반환하지 않습니다. 하위 클래스는 IMO가 아니더라도 스토리 보드에 연결하는 데 도움이됩니다. 모든 코드는 원래 클래스에 있어야합니다.
MyCustomView
입니다. 내 xib에서 왼쪽 내부 사이드 바는 기본적으로 누락되었습니다. 이 기능을 켜려면 하단 / 왼쪽 근처의 "보기 : iPhone 7"특성 컨트롤 옆에 단추가 있습니다.
나는 별도의 스토리 보드에서alternative
사용 XIB views
하는 것에 대해 생각 합니다.View Controller
그런 다음 사용자 정의보기 사용 대신에 주요 스토리 보드 container view
와 함께 Embed Segue
하고있는 StoryboardReference
이에 사용자 지정보기 컨트롤러 의 주요 스토리 보드에서 다른보기 내부에 배치해야 볼 수 있습니다.
그런 다음 segue 준비를 통해이 내장 ViewController와 기본 뷰 컨트롤러간에 위임 및 통신을 설정할 수 있습니다 . 이 접근 방식은 UIView를 표시하는 것과는 다르지만 프로그래밍 관점에서 볼 때 훨씬 간단하고 효율적으로 동일한 목표를 달성 할 수 있습니다. 즉, 메인 스토리 보드에서 볼 수있는 재사용 가능한 사용자 정의보기가 있습니다.
추가 이점은 CustomViewController 클래스에서 로직을 구현할 수 있으며 별도의 (프로젝트에서 찾기가 더 어려운) 컨트롤러 클래스를 만들지 않고 Component를 사용하여 기본 UIViewController에 상용구 코드를 배치하지 않고 모든 위임 및 뷰 준비를 설정할 수 있다는 것입니다. 나는 이것이 재사용 가능한 구성 요소에 좋다고 생각합니다. 다른보기에 포함 할 수있는 뮤직 플레이어 구성 요소 (위젯처럼).
가장 인기있는 답변은 잘 작동하지만 개념적으로 잘못되었습니다. 그들은 모두 File's owner
클래스의 콘센트와 UI 구성 요소 간의 연결로 사용 됩니다. s가 File's owner
아닌 최상위 개체에만 사용해야합니다 UIView
. Apple 개발자 문서를 확인하십시오 . UIView를 사용 File's owner
하면 이러한 바람직하지 않은 결과가 발생합니다.
contentView
곳에서 사용해야합니다self
합니다. 중간보기는 데이터 구조가 UI 구조를 전달하지 못하기 때문에 추악 할뿐만 아니라 구조적으로 잘못되었습니다. 선언적 UI와 반대입니다.를 사용하지 않고 우아한 방법이 있습니다 File's owner
. 이 블로그 게시물을 확인하십시오 . 올바른 방법으로 설명합니다.
여기 당신이 원했던 답이 있습니다. CustomView
클래스를 만들고 모든 서브 뷰와 아웃렛이있는 xib에 마스터 인스턴스를 만들 수 있습니다 . 그런 다음 스토리 보드 또는 다른 xib의 모든 인스턴스에 해당 클래스를 적용 할 수 있습니다.
File 's Owner와 바이올린을 연결하거나 콘센트를 프록시에 연결하거나 xib를 고유 한 방식으로 수정하거나 사용자 정의보기의 인스턴스를 자체의 하위보기로 추가 할 필요가 없습니다.
그냥 이렇게 :
UIView
하는 NibView
(또는에서 UITableViewCell
까지 NibTableViewCell
)그게 다야!
스토리 보드에서 디자인 타임에 사용자 정의보기 (xib의 하위보기 포함)를 참조하기 위해 IBDesignable 과도 작동합니다.
https://medium.com/build-an-app-like-lego/embed-a-xib-in-a-storyboard-953edf274155에 대한 자세한 내용은 여기를 참조 하십시오.
https://github.com/BareFeetWare/BFWControls 에서 오픈 소스 BFWControls 프레임 워크를 얻을 수 있습니다.
NibReplaceable
궁금한 경우를 대비 하여 코드를 구동 하는 간단한 코드 추출은 다음과 같습니다.
https://gist.github.com/barefeettom/f48f6569100415e0ef1fd530ca39f5b4
톰 👣
이 솔루션은 클래스 이름이 XIB와 동일하지 않은 경우에도 사용할 수 있습니다. 예를 들어, XIB 이름이 controllerA.xib 인 기본보기 컨트롤러 클래스 controllerA가 있고이를 controllerB로 서브 클래 싱하고 스토리 보드에서 controllerB의 인스턴스를 작성하려는 경우 다음을 수행 할 수 있습니다.
*
- (void) loadView
{
//according to the documentation, if a nibName was passed in initWithNibName or
//this controller was created from a storyboard (and the controller has a view), then nibname will be set
//else it will be nil
if (self.nibName)
{
//a nib was specified, respect that
[super loadView];
}
else
{
//if no nib name, first try a nib which would have the same name as the class
//if that fails, force to load from the base class nib
//this is convenient for including a subclass of this controller
//in a storyboard
NSString *className = NSStringFromClass([self class]);
NSString *pathToNIB = [[NSBundle bundleForClass:[self class]] pathForResource: className ofType:@"nib"];
UINib *nib ;
if (pathToNIB)
{
nib = [UINib nibWithNibName: className bundle: [NSBundle bundleForClass:[self class]]];
}
else
{
//force to load from nib so that all subclass will have the correct xib
//this is convenient for including a subclass
//in a storyboard
nib = [UINib nibWithNibName: @"baseControllerXIB" bundle:[NSBundle bundleForClass:[self class]]];
}
self.view = [[nib instantiateWithOwner:self options:nil] objectAtIndex:0];
}
}
Ben Patch의 응답에 설명 된 단계에 따른 Objective-C 솔루션 .
UIView 확장을 사용하십시오.
@implementation UIView (NibLoadable)
- (UIView*)loadFromNib
{
UIView *xibView = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] firstObject];
xibView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:xibView];
[xibView.topAnchor constraintEqualToAnchor:self.topAnchor].active = YES;
[xibView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor].active = YES;
[xibView.leftAnchor constraintEqualToAnchor:self.leftAnchor].active = YES;
[xibView.rightAnchor constraintEqualToAnchor:self.rightAnchor].active = YES;
return xibView;
}
@end
파일을 작성 MyView.h
, MyView.m
하고 MyView.xib
.
먼저 당신의 준비 MyView.xib
로 벤 패치의 반응이 너무 세트 클래스 말한다 MyView
대신 XIB 내부 기본보기의 파일의 소유자가.
MyView.h
:
#import <UIKit/UIKit.h>
IB_DESIGNABLE @interface MyView : UIView
@property (nonatomic, weak) IBOutlet UIView* someSubview;
@end
MyView.m
:
#import "MyView.h"
#import "UIView+NibLoadable.h"
@implementation MyView
#pragma mark - Initializers
- (id)init
{
self = [super init];
if (self) {
[self loadFromNib];
[self internalInit];
}
return self;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self loadFromNib];
[self internalInit];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
[self loadFromNib];
}
return self;
}
- (void)awakeFromNib
{
[super awakeFromNib];
[self internalInit];
}
- (void)internalInit
{
// Custom initialization.
}
@end
나중에 프로그래밍 방식으로 뷰를 만듭니다.
MyView* view = [[MyView alloc] init];
경고! Xcode> = 9.2의이 버그로 인해 WatchKit Extension을 사용하는 경우이보기의 미리보기가 스토리 보드에 표시되지 않습니다 : https://forums.developer.apple.com/thread/95616