EasyStackView 1.3.0

EasyStackView 1.3.0

Hanping Xu 维护。



EasyStackView

version license platform Swift Objecgive-C

简介

本项目为 iOS 原生开发提供了一种简单优雅的 flex 布局实现。此项目可以与 Swift 和 Objective-C 语言一起使用。

如果您喜欢,请星标

Cocoapods 集成

使用该工具找到您的项目中的 podfile 脚本。

    pod 'EasyStackView'

如何使用

目前有四种类型的 flex 容器

ESVStackView -> UIView

ESVStackPlaceHolder -> UIView(在使用弹性布局时,我们创建一些仅用于布局参考的 <div>。为了避免创建过多的无用的 UIView 对象,请使用 ESVStackPlaceHolder 而不是 ESVStackView。与 ESVStackView 相比,ESVStackPlaceHolder 要少得多。)

注意: ESVStackPlaceHolder 实例不应作为柔性布局树的根。)

ESVScrollView -> UIScrollView

ESVRecycleView -> UITableViewUICollectionView

示例

Alert

构建与上图相同的 UI 只需几个步骤。

  1. 首先,为整个 alertView 创建一个容器。容器内的每个元素按垂直方向渲染,并尽可能拉伸以填充宽度。
let container: ESVStackView = createContainer()
container.flexDirection = .column
container.alignItems = .stretch
  1. 添加标题。
let title: UILabel = createTitle()
container.addArrangedItem(title)
  1. 添加内容并设置内容的间距。
let content: UITextView = createContent()
container.addArrangedItem(content)
container.manageConfig(of: content) { (config) in
    config?.margin = .init(top: 10, left: 10, bottom: 10, right: 10)
    config?.growth = true
}
  1. 创建一个可滚动的容器以包含四个图标。隐藏水平滚动指示器。元素横向布局。
let iconHolder: ESVScrollView = createIconHolder()
iconHolder.showsHorizontalScrollIndicator = false
container.addArrangedItem(iconHolder)
iconHolder.flexDirection = .row
container.manageConfig(of: iconHolder) { (config) in
    config?.margin = .init(top: 10, left: 10, bottom: 10, right: 10)
}
  1. 创建四个项目并将它们添加到可滚动的容器中。
for i in 0...4 {
    let icon: UIView = createIcon()
    if i % 3 == 0 {
        icon.backgroundColor = UIColor.red
    } else {
        icon.backgroundColor = UIColor.white
    }
    iconHolder.addArrangedItem(icon)
    if i == 4 {
        iconHolder.manageConfig(of: UInt(i)) { (config) in
            config?.margin = .init(top: 0, left: 5, bottom: 0, right: 5)
        }
    } else {
        iconHolder.manageConfig(of: UInt(i)) { (config) in
            config?.margin = .init(top: 0, left: 5, bottom: 0, right: 0)
        }
    }
}
  1. 创建另一个可滚动的容器以包含十个图标。如果有太多的图标,我们更愿意使用可循环容器。
let recycleIconHolder: ESVRecycleView = createRecycleIconHolder()
recycleIconHolder.showsHorizontalScrollIndicator = false
container.addArrangedItem(recycleIconHolder)
recycleIconHolder.flexDirection = .row
container.manageConfig(of: recycleIconHolder) { (config) in
    config?.margin = .init(top: 10, left: 10, bottom: 10, right: 10)
}
  1. 定义一个 UIView 的子类,将其作为显示中的可循环元素使用。
class RecycleIcon: UIView, ESVRecycleCellType {
    func prepareForReuse() {
        
    }
    
    func config(with model: ESVRecyclableModelType, index: UInt) {
        self.layer.cornerRadius = 6
        self.backgroundColor = index % 3 == 0 ? UIColor.white : UIColor.red
    }
}
  1. 使用键 "RecycleIcon" 注册 RecycleIcon 生成器。
recycleIconHolder.registerGenerator({ () -> UIView & ESVRecycleCellType in
    return RecycleIcon()
}, forIdentifier: "RecycleIcon")
  1. 将十个模型对象添加到容器中。
for i in 0...10 {
    let model: ESVRecyclableModelType = ESVRecyclableModel()
    model.frame = .init(x: 0, y: 0, width: 50, height: 50)
    model.identifier = "RecycleIcon"
    recycleIconHolder.addArrangedItem(model)
    if i == 10 {
        recycleIconHolder.manageConfig(of: UInt(i)) { (config) in
            config?.margin = .init(top: 0, left: 5, bottom: 0, right: 5)
        }
    } else {
        recycleIconHolder.manageConfig(of: UInt(i)) { (config) in
            config?.margin = .init(top: 0, left: 5, bottom: 0, right: 0)
        }
    }
}
  1. 添加按钮容器。它仅用作渲染参考,不具有显示功能。因此,使用 ESVStackPlaceHolder 是一个经济实惠的选择。
let buttonContainer: ESVStackPlaceHolder = createButtonContainer()
container.addArrangedItem(buttonContainer)
buttonContainer.flexDirection = .row
buttonContainer.alignItems = .stretch
  1. 将两个按钮添加到按钮容器中。两个按钮大小相等,应拉伸以填充所有空间。
let confirmButton: UIButton = createConfirmButton()
buttonContainer.addArrangedItem(confirmButton)
buttonContainer.manageConfig(of: confirmButton) { (config) in
    config?.growth = true
    config?.margin = .init(top: 5, left: 5, bottom: 5, right: 5)
}
let cancelButton: UIButton = createCancelButton()
buttonContainer.addArrangedItem(cancelButton)
buttonContainer.manageConfig(of: cancelButton) { (config) in
    config?.growth = true
    config?.margin = .init(top: 5, left: 5, bottom: 5, right: 5)
}

此示例编写在此存储库的 ./Demo/Sample4ViewController.swift 中。您可以使用 Xcode 运行 Demo 目标以检查此内容。

特性


所有容器都遵守 ESVFlexManageType 协议,这为它们提供了布局属性。

@protocol ESVFlexManageType <NSObject>

/// Flex direction.
@property (nonatomic, assign) ESVDirection flexDirection;

/// Distribution mode along axis.
@property (nonatomic, assign) ESVJustify justifyContent;

/// Distribution mode cross axis.
@property (nonatomic, assign) ESVAlign alignItems;

/// Minium space between items.
@property (nonatomic, assign) CGFloat spaceBetween;

@end

所有容器都遵守 ESVItemManageType 协议,这为它们提供了管理项目的能力。

@protocol ESVItemManageType <NSObject>

/// All items arranged in flex layout.
@property (nonatomic, readonly) NSArray<__kindof NSObject<ESVStackItemType> *> *arrangedItems;

/// Add item at the end of arranged subviews.
/// @param item Item to add
- (void)addArrangedItem:(NSObject<ESVStackItemType> *)item;

/// Delete item from arranged subview.
/// @param item Item to delete.
- (void)deleteArrangedItem:(NSObject<ESVStackItemType> *)item;

/// Insert item into arranged subviews.
/// @param item Item to insert.
/// @param index Index to insert.
- (void)insertArrangedItem:(NSObject<ESVStackItemType> *)item atIndex:(NSUInteger)index;

@property (nonatomic, readonly) NSArray<__kindof UIView *> *managedViews;

@property (nonatomic, weak) NSObject<ESVStackItemType> * superItem;
 
@end

注意: 只有使用函数 - (void)addArrangedItem:(NSObject<ESVStackItemType> *)item; 添加的对象才能加入柔性布局。使用 -(void)addSubview:(UIView *)view 添加的视图将以与普通视图相同的方式进行布局。


所有容器都遵守 ESVRefreshManageType 协议,这提供了关于渲染的一些操作。

@protocol ESVRefreshManageType <NSObject>

@property (readonly) BOOL dirty;

/// Mark this item as dirty, it layout soon after.
- (void)markAsDirty;

/// Try to all layout subviews if self or arranged items are dirty.
- (void)render;

/// This should not be called mannually.
- (void)applyItemFrame;

/// The preffered size of this view, which can properly hold all its arranged items.
@property (readonly) CGSize preferedSize;

@end

所有容器都遵守 ESVConfigManageType 协议,这提供了操作排列项目配置的方法。

@protocol ESVConfigManageType <ESVItemManageType>

/// Settle config of item. -setNeedLayout will be called automatically after change config.
/// @param item The item you want to change config of.
/// @param configAction Change config in this block, will be called synchronized. There is no retain circle problem.
- (void)manageConfigOfItem:(NSObject<ESVStackItemType> *)item configAction:(void(^)(ESVStackItemConfig * _Nullable config))configAction;

/// Settle config of item. -setNeedLayout will be called automatically after change config.
/// @param index The index you want to change config of.
/// @param configAction Change config in this block, will be called synchronized. There is no retain circle problem.
- (void)manageConfigOfIndex:(NSUInteger)index configAction:(void(^)(ESVStackItemConfig * _Nullable config))configAction;

/// Get config of item. You must call -setNeedLayout by yourself to layout again.
/// @param item The item.
- (nullable ESVStackItemConfig *)configOfItem:(NSObject<ESVStackItemType> *)item;

/// Get config of index. You must call -setNeedLayout by yourself to layout again.
/// @param index The index of config.
- (nullable ESVStackItemConfig *)configOfIndex:(NSUInteger)index;

@end

每个排列项都与它的 ESVStackItemConfig 配置对象相关联。您可以使用 ESVStackItemConfig 实例指定此排列项的布局属性。

@interface ESVStackItemConfig : NSObject

#pragma mark - Cache

/// Frame cache of view. DO NOT change this property value unless you are pretty sure what you are doing.
@property (nonatomic, assign) CGRect cacheTransformedFrame;

@property (nonatomic, assign) CGRect cacheFrame;

#pragma mark - Associate

/// The view this config is associated to.
@property (nonatomic, readonly) NSObject<ESVStackItemType> * item;

#pragma mark - Config

/// Is this view layouted according to alignSelf in this config. Default is false.
@property (nonatomic, assign) BOOL alignSelfOn;

/// Describe how to layout this view in flex layout. Only work when alignSelfOn is true.
@property (nonatomic, assign) ESVAlign alignSelf;

/// Orignal frame of the view. This is the reference size of the view during layout. Default is view's size when added into arranged views array.
@property (nonatomic, assign) CGSize originSize;

/// Minium margin inset of view in four direction. Default is {0, 0, 0, 0,}.
@property (nonatomic, assign) UIEdgeInsets margin;

/// Indicates whether this view will auto stretch when super view is not filled along axis. Default is false.
@property (nonatomic, assign) BOOL growth;

/// Indicates whether this view will auto shrink when super view is too narrow along axis. Default is false.
@property (nonatomic, assign) BOOL shrink;

- (instancetype)initWithItem:(NSObject<ESVStackItemType> *)item;

@end

待办事项

【完成】带有弹性布局的滚动视图。

【完成】带有可重用视图的滚动视图,以便替换 UITableView

【待办】尝试使 EasyStackView 的行为与 Web 弹性布局基本相同。