рдирдорд╕реНрдХрд╛рд░, рд╣реЗрдмреНрд░! рдореЗрд░рд╛ рдирд╛рдо рдирд┐рдХрд┐рддрд╛ рд╣реИ, рдореИрдВ рдПрдмреАрдмреАрд╡рд╛рдИ рдореЗрдВ рдореЛрдмрд╛рдЗрд▓ рдПрд╕рдбреАрдХреЗ рдкрд░ рдХрд╛рдо рдХрд░рддрд╛ рд╣реВрдВ, рдФрд░ рдореИрдВ рд╕реНрдорд╛рд░реНрдЯрдлрд╝реЛрди рдкрд░ рдорд▓реНрдЯреА-рдкреЗрдЬ рджрд╕реНрддрд╛рд╡реЗрдЬрд╝ рд╕реНрдХреИрдирд┐рдВрдЧ рдФрд░ рдЖрд╕рд╛рдиреА рд╕реЗ рджреЗрдЦрдиреЗ рдХреЗ рд▓рд┐рдП рдпреВрдЖрдИ рдШрдЯрдХ рдХреЗ рд╕рд╛рде рднреА рдХрд╛рдо рдХрд░рддрд╛ рд╣реВрдВред рдпрд╣ рдШрдЯрдХ
ABBYY рдореЛрдмрд╛рдЗрд▓ рдХреИрдкреНрдЪрд░ рддрдХрдиреАрдХ рдкрд░ рдЖрдзрд╛рд░рд┐рдд рдЕрдиреБрдкреНрд░рдпреЛрдЧреЛрдВ рдХреЗ рд╡рд┐рдХрд╛рд╕ рдХреЗ рд╕рдордп рдХреЛ рдХрдо рдХрд░рддрд╛ рд╣реИ рдФрд░ рдЗрд╕рдореЗрдВ рдХрдИ рднрд╛рдЧ рд╣реЛрддреЗ рд╣реИрдВред рд╕рдмрд╕реЗ рдкрд╣рд▓реЗ, рджрд╕реНрддрд╛рд╡реЗрдЬреЛрдВ рдХреЛ рд╕реНрдХреИрди рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдПрдХ рдХреИрдорд░рд╛; рджреВрд╕рд░реЗ, рдХреИрдкреНрдЪрд░ рдкрд░рд┐рдгрд╛рдореЛрдВ рдХреЗ рд╕рд╛рде рдПрдХ рд╕рдВрдкрд╛рджрдХ рд╕реНрдХреНрд░реАрди (рдЬреЛ рд╕реНрд╡рдЪрд╛рд▓рд┐рдд рд░реВрдк рд╕реЗ рдлрд╝реЛрдЯреЛ рд▓реА рдЧрдИ рд╣реИ) рдФрд░ рджрд╕реНрддрд╛рд╡реЗрдЬрд╝ рдХреА рд╕реАрдорд╛рдУрдВ рдХреЛ рд╕рд╣реА рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдПрдХ рд╕реНрдХреНрд░реАрдиред
рдпрд╣ рдбреЗрд╡рд▓рдкрд░ рдХреЗ рд▓рд┐рдП рдХреБрдЫ рддрд░реАрдХреЛрдВ рдХреЛ рдХреЙрд▓ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдкрд░реНрдпрд╛рдкреНрдд рд╣реИ - рдФрд░ рдЕрдм рдЙрд╕рдХреЗ рдЖрд╡реЗрджрди рдореЗрдВ рдПрдХ рдХреИрдорд░рд╛ рдкрд╣рд▓реЗ рд╕реЗ рд╣реА рдЙрдкрд▓рдмреНрдз рд╣реИ рдЬреЛ рд╕реНрд╡рдЪрд╛рд▓рд┐рдд рд░реВрдк рд╕реЗ рджрд╕реНрддрд╛рд╡реЗрдЬреЛрдВ рдХреЛ рд╕реНрдХреИрди рдХрд░рддрд╛ рд╣реИред рд▓реЗрдХрд┐рди, рдХреЙрдиреНрдлрд╝рд┐рдЧрд░ рдХрд┐рдП рдЧрдП рдХреИрдорд░реЛрдВ рдХреЗ рдЕрд▓рд╛рд╡рд╛, рдЖрдкрдХреЛ рдЧреНрд░рд╛рд╣рдХреЛрдВ рдХреЛ рд╕реНрдХреИрди рдкрд░рд┐рдгрд╛рдореЛрдВ рдХреЗ рд▓рд┐рдП рд╕реБрд╡рд┐рдзрд╛рдЬрдирдХ рдкрд╣реБрдВрдЪ рдкреНрд░рджрд╛рди рдХрд░рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ, рдЕрд░реНрдерд╛рддред рд╕реНрд╡рдЪрд╛рд▓рд┐рдд рд░реВрдк рд╕реЗ рдлрд╝реЛрдЯреЛ рд▓рд┐рдпрд╛ рдЧрдпрд╛ред рдФрд░ рдЕрдЧрд░ рдХреНрд▓рд╛рдЗрдВрдЯ рдХреЙрдиреНрдЯреНрд░реИрдХреНрдЯ рдпрд╛ рдЪрд╛рд░реНрдЯрд░ рдХреЛ рд╕реНрдХреИрди рдХрд░рддрд╛ рд╣реИ, рддреЛ рдЗрд╕ рддрд░рд╣ рдХреА рдмрд╣реБрдд рд╕рд╛рд░реА рддрд╕реНрд╡реАрд░реЗрдВ рд╣реЛ рд╕рдХрддреА рд╣реИрдВред
рдЗрд╕ рдкреЛрд╕реНрдЯ рдореЗрдВ рдореИрдВ рдЙрди рдХрдард┐рдирд╛рдЗрдпреЛрдВ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдмрд╛рдд рдХрд░реВрдВрдЧрд╛ рдЬреЛ рджрд╕реНрддрд╛рд╡реЗрдЬрд╝ рдХреИрдкреНрдЪрд░ рдХреЗ рдкрд░рд┐рдгрд╛рдореЛрдВ рдХреЗ рд╕рд╛рде рд╕рдВрдкрд╛рджрдХ рд╕реНрдХреНрд░реАрди рдХреЗ рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рдХреЗ рджреМрд░рд╛рди рдЙрддреНрдкрдиреНрди рд╣реБрдИ рдереАрдВред рд╕реНрдХреНрд░реАрди рд╣реА рдПрдХ рджреЛ
UICollectionView
, рдореИрдВ рдЙрдиреНрд╣реЗрдВ рдмрдбрд╝реЗ рдФрд░ рдЫреЛрдЯреЗ рдлреЛрди рдХрд░реВрдВрдЧрд╛ред рдореИрдВ рджрд╕реНрддрд╛рд╡реЗрдЬрд╝ рдХреЗ рд╕рд╛рде рдореИрдиреНрдпреБрдЕрд▓ рд░реВрдк рд╕реЗ рджрд╕реНрддрд╛рд╡реЗрдЬрд╝ рдХреА рд╕реАрдорд╛рдУрдВ рдФрд░ рдЕрдиреНрдп рдХрд╛рд░реНрдпреЛрдВ рдХреЛ рд╕рдорд╛рдпреЛрдЬрд┐рдд рдХрд░рдиреЗ рдХреА рд╕рдВрднрд╛рд╡рдирд╛рдУрдВ рдХреЛ рдЫреЛрдбрд╝ рджреВрдВрдЧрд╛, рдФрд░ рдореИрдВ рд╕реНрдХреНрд░реЙрд▓ рдХреЗ рджреМрд░рд╛рди рдПрдирд┐рдореЗрд╢рди рдФрд░ рд▓реЗрдЖрдЙрдЯ рд╕реБрд╡рд┐рдзрд╛рдУрдВ рдкрд░ рдзреНрдпрд╛рди рдХреЗрдВрджреНрд░рд┐рдд рдХрд░реВрдВрдЧрд╛ред рдиреАрдЪреЗ GIF рдкрд░ рдЖрдк рджреЗрдЦ рд╕рдХрддреЗ рд╣реИрдВ рдХрд┐ рдЕрдВрдд рдореЗрдВ рдХреНрдпрд╛ рд╣реБрдЖред рд░рд┐рдкреЙрдЬрд┐рдЯрд░реА рдХрд╛ рдПрдХ рд▓рд┐рдВрдХ рд▓реЗрдЦ рдХреЗ рдЕрдВрдд рдореЗрдВ рд╣реЛрдЧрд╛ред

рд╕рдВрджрд░реНрдн рдХреЗ рд░реВрдк рдореЗрдВ, рдореИрдВ рдЕрдХреНрд╕рд░ Apple рд╕рд┐рд╕реНрдЯрдо рдЕрдиреБрдкреНрд░рдпреЛрдЧреЛрдВ рдкрд░ рдзреНрдпрд╛рди рджреЗрддрд╛ рд╣реВрдВред рдЬрдм рдЖрдк рдзреНрдпрд╛рди рд╕реЗ рдЙрдирдХреЗ рдЕрдиреБрдкреНрд░рдпреЛрдЧреЛрдВ рдХреЗ рдПрдирд┐рдореЗрд╢рди рдФрд░ рдЕрдиреНрдп рдЗрдВрдЯрд░рдлрд╝реЗрд╕ рд╕рдорд╛рдзрд╛рдиреЛрдВ рдХреЛ рджреЗрдЦрддреЗ рд╣реИрдВ, рддреЛ рдЖрдк рд╡рд┐рднрд┐рдиреНрди trifles рдХреЗ рд▓рд┐рдП рдЙрдирдХреЗ рдЪреМрдХрд╕ рд░рд╡реИрдпреЗ рдХреА рдкреНрд░рд╢рдВрд╕рд╛ рдХрд░рдирд╛ рд╢реБрд░реВ рдХрд░рддреЗ рд╣реИрдВред рдЕрдм рд╣рдо рдПрдХ рд╕рдВрджрд░реНрдн рдХреЗ рд░реВрдк рдореЗрдВ
рдлреЛрдЯреЛ рдПрдкреНрд▓реАрдХреЗрд╢рди (iOS 12) рдХреЛ рджреЗрдЦреЗрдВрдЧреЗред рдореИрдВ рдЗрд╕ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреА рд╡рд┐рд╢рд┐рд╖реНрдЯ рд╡рд┐рд╢реЗрд╖рддрд╛рдУрдВ рдкрд░ рдЖрдкрдХрд╛ рдзреНрдпрд╛рди рдЖрдХрд░реНрд╖рд┐рдд рдХрд░реВрдВрдЧрд╛, рдФрд░ рдлрд┐рд░ рд╣рдо рдЙрдиреНрд╣реЗрдВ рд▓рд╛рдЧреВ рдХрд░рдиреЗ рдХрд╛ рдкреНрд░рдпрд╛рд╕ рдХрд░реЗрдВрдЧреЗред
рд╣рдо
UICollectionViewFlowLayout
рдЕрдзрд┐рдХрд╛рдВрд╢ рдЕрдиреБрдХреВрд▓рди
UICollectionViewFlowLayout
рдХреЛ рдХрд╡рд░ рдХрд░реЗрдВрдЧреЗ, рджреЗрдЦреЗрдВ рдХрд┐ рдХреИрд╕реЗ рд▓рдВрдмрди рдФрд░ рд╣рд┐рдВрдбреЛрд▓рд╛ рдЬреИрд╕реА рд╕рд╛рдорд╛рдиреНрдп рддрдХрдиреАрдХреЛрдВ рдХреЛ рд▓рд╛рдЧреВ рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИ, рдФрд░ рдХреЛрд╢рд┐рдХрд╛рдУрдВ рдХреЛ рдбрд╛рд▓рдиреЗ рдФрд░ рд╣рдЯрд╛рдиреЗ рдХреЗ рджреМрд░рд╛рди рдХрд╕реНрдЯрдо рдПрдирд┐рдореЗрд╢рди рд╕реЗ рдЬреБрдбрд╝реА рд╕рдорд╕реНрдпрд╛рдУрдВ рдкрд░ рдЪрд░реНрдЪрд╛ рдХреА рдЬрд╛рддреА рд╣реИред
рд╕реБрд╡рд┐рдзрд╛рдПрдБ рд╕рдореАрдХреНрд╖рд╛
рдмрд╛рд░реАрдХрд┐рдпреЛрдВ рдХреЛ рдЬреЛрдбрд╝рдиреЗ рдХреЗ рд▓рд┐рдП, рдореИрдВ рд╡рд░реНрдгрди рдХрд░реВрдВрдЧрд╛ рдХрд┐
рдлрд╝реЛрдЯреЛ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдореЗрдВ рдХрд┐рди рд╡рд┐рд╢рд┐рд╖реНрдЯ рдЫреЛрдЯреА рдЪреАрдЬрд╝реЛрдВ рдиреЗ рдореБрдЭреЗ рдкреНрд░рд╕рдиреНрди рдХрд┐рдпрд╛, рдФрд░ рдлрд┐рд░ рдореИрдВ рдЙрдиреНрд╣реЗрдВ рдЙрдкрдпреБрдХреНрдд рдХреНрд░рдо рдореЗрдВ рд▓рд╛рдЧреВ рдХрд░реВрдВрдЧрд╛ред
- рдПрдХ рдмрдбрд╝реЗ рд╕рдВрдЧреНрд░рд╣ рдореЗрдВ рд▓рдВрдмрди рдкреНрд░рднрд╛рд╡
- рдПрдХ рдЫреЛрдЯреЗ рд╕рдВрдЧреНрд░рд╣ рдХреЗ рддрддреНрд╡ рдХреЗрдВрджреНрд░рд┐рдд рд╣реИрдВред
- рдПрдХ рдЫреЛрдЯреЗ рд╕рдВрдЧреНрд░рд╣ рдореЗрдВ рд╡рд╕реНрддреБрдУрдВ рдХрд╛ рдЧрддрд┐рд╢реАрд▓ рдЖрдХрд╛рд░
- рдПрдХ рдЫреЛрдЯреЗ рд╕реЗрд▓ рдХреЗ рддрддреНрд╡реЛрдВ рдХреЛ рд░рдЦрдиреЗ рдХрд╛ рддрд░реНрдХ рди рдХреЗрд╡рд▓ рд╕рд╛рдордЧреНрд░реА рдкрд░ рдирд┐рд░реНрднрд░ рдХрд░рддрд╛ рд╣реИ, рдмрд▓реНрдХрд┐ рдЙрдкрдпреЛрдЧрдХрд░реНрддрд╛ рдХреА рдмрд╛рддрдЪреАрдд рдкрд░ рднреА рдирд┐рд░реНрднрд░ рдХрд░рддрд╛ рд╣реИ
- рдЪрд╛рд▓ рдФрд░ рд╣рдЯрд╛рдиреЗ рдХреЗ рд▓рд┐рдП рдХрд╕реНрдЯрдо рдПрдирд┐рдореЗрд╢рди
- рдЕрднрд┐рд╡рд┐рдиреНрдпрд╛рд╕ рдмрджрд▓рддреЗ рд╕рдордп "рд╕рдХреНрд░рд┐рдп" рд╕реЗрд▓ рдХрд╛ рд╕реВрдЪрдХрд╛рдВрдХ рдЦреЛ рдирд╣реАрдВ рдЬрд╛рддрд╛ рд╣реИ
1. рд▓рдВрдмрди
рд▓рдВрдмрди рдХреНрдпрд╛ рд╣реИ?
рд▓рдВрдмрди рд╕реНрдХреНрд░реЙрд▓рд┐рдВрдЧ рдХрдВрдкреНрдпреВрдЯрд░ рдЧреНрд░рд╛рдлрд┐рдХреНрд╕ рдХреА рдПрдХ рддрдХрдиреАрдХ рд╣реИ рдЬрд┐рд╕рдореЗрдВ рдмреИрдХрдЧреНрд░рд╛рдЙрдВрдб рдЗрдореЗрдЬреЗрдЬ рдХреИрдорд░реЗ рдХреЛ рдЕрдЧреНрд░рднреВрдорд┐ рдЪрд┐рддреНрд░реЛрдВ рдХреА рддреБрд▓рдирд╛ рдореЗрдВ рдзреАрд░реЗ-рдзреАрд░реЗ рдЖрдЧреЗ рдмрдврд╝рд╛рддреЗ рд╣реИрдВ, рдЬрд┐рд╕рд╕реЗ 2 рдбреА рджреГрд╢реНрдп рдореЗрдВ рдЧрд╣рд░рд╛рдИ рдХрд╛ рднреНрд░рдо рдкреИрджрд╛ рд╣реЛрддрд╛ рд╣реИ рдФрд░ рдЖрднрд╛рд╕реА рдЕрдиреБрднрд╡ рдореЗрдВ рд╡рд┐рд╕рд░реНрдЬрди рдХреА рднрд╛рд╡рдирд╛ рдЬреБрдбрд╝ рдЬрд╛рддреА рд╣реИред
рдЖрдк рджреЗрдЦ рд╕рдХрддреЗ рд╣реИрдВ рдХрд┐ рд╕реНрдХреНрд░реЙрд▓ рдХрд░рддреЗ рд╕рдордп, рд╕реЗрд▓ рдХрд╛ рдлреНрд░реЗрдо рдЙрд╕ рдЫрд╡рд┐ рдХреА рддреБрд▓рдирд╛ рдореЗрдВ рдЕрдзрд┐рдХ рддреЗрдЬ рдЪрд▓рддрд╛ рд╣реИред
рдЪрд▓реЛ рд╢реБрд░реВ рд╣реЛ рдЬрд╛рдУ! рд╕реЗрд▓ рдХрд╛ рдПрдХ рдЙрдкрд╡рд░реНрдЧ рдмрдирд╛рдПрдБ, рдЗрд╕рдореЗрдВ UIImageView рдбрд╛рд▓реЗрдВред
class PreviewCollectionViewCell: UICollectionViewCell { private(set) var imageView = UIImageView()тАЛ override init(frame: CGRect) { super.init(frame: frame) addSubview(imageView) clipsToBounds = true imageView.snp.makeConstraints { $0.edges.equalToSuperview() } } }
class PreviewCollectionViewCell: UICollectionViewCell { private(set) var imageView = UIImageView()тАЛ override init(frame: CGRect) { super.init(frame: frame) addSubview(imageView) clipsToBounds = true imageView.snp.makeConstraints { $0.edges.equalToSuperview() } } }
рдЕрдм рдЖрдкрдХреЛ рдпрд╣ рд╕рдордЭрдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ рдХрд┐ рд▓рдВрдмрди рдкреНрд░рднрд╛рд╡ рдмрдирд╛рддреЗ рд╣реБрдП
imageView
рдХреЛ рдХреИрд╕реЗ рд╕реНрдерд╛рдирд╛рдВрддрд░рд┐рдд рдХрд┐рдпрд╛
imageView
ред рдРрд╕рд╛ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рдЖрдкрдХреЛ рд╕реНрдХреНрд░реЙрд▓рд┐рдВрдЧ рдХреЗ рджреМрд░рд╛рди рдХреЛрд╢рд┐рдХрд╛рдУрдВ рдХреЗ рд╡реНрдпрд╡рд╣рд╛рд░ рдХреЛ рдлрд┐рд░ рд╕реЗ рдкрд░рд┐рднрд╛рд╖рд┐рдд рдХрд░рдирд╛ рд╣реЛрдЧрд╛ред рдПрдкреНрдкрд▓:
UICollectionView
рдЙрдкрд╡рд░реНрдЧ рд╕реЗ рдмрдЪреЗрдВред рд╕рдВрдЧреНрд░рд╣ рдХреЗ рджреГрд╢реНрдп рдХрд╛ рдЕрдкрдирд╛ рдХреЛрдИ рд░реВрдк рдирд╣реАрдВ рд╣реИред рдЗрд╕рдХреЗ рдмрдЬрд╛рдп, рдпрд╣ рдЖрдкрдХреЗ рдбреЗрдЯрд╛ рд╕реНрд░реЛрдд рдСрдмреНрдЬреЗрдХреНрдЯ рдФрд░ рд▓реЗрдЖрдЙрдЯ рдСрдмреНрдЬреЗрдХреНрдЯ рд╕реЗ рд╕рднреА рд▓реЗрдЖрдЙрдЯ-рд╕рдВрдмрдВрдзрд┐рдд рдЬрд╛рдирдХрд╛рд░реА рд╕реЗ рдЕрдкрдиреЗ рд╕рднреА рд╡рд┐рдЪрд╛рд░реЛрдВ рдХреЛ рдЦреАрдВрдЪрддрд╛ рд╣реИред рдпрджрд┐ рдЖрдк рддреАрди рдЖрдпрд╛рдореЛрдВ рдореЗрдВ рдЖрдЗрдЯрдо рдмрд╛рд╣рд░ рдХрд░рдиреЗ рдХреА рдХреЛрд╢рд┐рд╢ рдХрд░ рд░рд╣реЗ рд╣реИрдВ, рддреЛ рдРрд╕рд╛ рдХрд░рдиреЗ рдХрд╛ рдЙрдЪрд┐рдд рддрд░реАрдХрд╛ рдПрдХ рдХрд╕реНрдЯрдо рд▓реЗрдЖрдЙрдЯ рдХреЛ рд▓рд╛рдЧреВ рдХрд░рдирд╛ рд╣реИ рдЬреЛ рдкреНрд░рддреНрдпреЗрдХ рд╕реЗрд▓ рдХреЗ 3 рдбреА рдкрд░рд┐рд╡рд░реНрддрди рдХреЛ рд▓рд╛рдЧреВ рдХрд░рддрд╛ рд╣реИ рдФрд░ рдЙрдЪрд┐рдд рд░реВрдк рд╕реЗ рджреЗрдЦрддрд╛ рд╣реИред
рдареАрдХ рд╣реИ, рдЪрд▓реЛ рд╣рдорд╛рд░реА
рд▓реЗрдЖрдЙрдЯ рдСрдмреНрдЬреЗрдХреНрдЯ рдмрдирд╛рддреЗ рд╣реИрдВред
UICollectionView
рдкрд╛рд╕ рдПрдХ рд╕рдВрдкрддреНрддрд┐
collectionViewLayout
UICollectionView
, рдЬрд┐рд╕рдореЗрдВ рд╕реЗ рдпрд╣ рдХреЛрд╢рд┐рдХрд╛рдУрдВ рдХреА рд╕реНрдерд┐рддрд┐ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдЬрд╛рдирдХрд╛рд░реА рдкреНрд░рд╛рдкреНрдд рдХрд░рддрд╛ рд╣реИред
UICollectionViewFlowLayout
рд╕рд╛рд░
UICollectionViewFlowLayout
рдХрд╛ рдПрдХ рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рд╣реИ, рдЬреЛ
collectionViewLayout
UICollectionViewLayout
рдЧреБрдг рд╣реИред
UICollectionViewLayout
рдХрд┐рд╕реА рдХреЗ рд▓рд┐рдП рдЗрд╕реЗ рдЙрдк-рд╡рд░реНрдЧ рдХрд░рдиреЗ рдФрд░ рдЙрдЪрд┐рдд рд╕рд╛рдордЧреНрд░реА рдкреНрд░рджрд╛рди рдХрд░рдиреЗ рдХреА рдкреНрд░рддреАрдХреНрд╖рд╛ рдХрд░ рд░рд╣рд╛ рд╣реИред UICollectionViewFlowLayout
, UICollectionViewFlowLayout
рдХрд╛ рдПрдХ рдареЛрд╕ рд╡рд░реНрдЧ рд╣реИ, UICollectionViewLayout
рд╕рднреА рдЪрд╛рд░ рд╕рджрд╕реНрдпреЛрдВ рдХреЛ рдХрд╛рд░реНрдпрд╛рдиреНрд╡рд┐рдд рдХрд┐рдпрд╛ рдЧрдпрд╛ рд╣реИ, рдЬрд┐рд╕ рддрд░рд╣ рд╕реЗ рдХреЛрд╢рд┐рдХрд╛рдУрдВ рдХреЛ рдЧреНрд░рд┐рдб рддрд░реАрдХреЗ рд╕реЗ рд╡реНрдпрд╡рд╕реНрдерд┐рдд рдХрд┐рдпрд╛ рдЬрд╛рдПрдЧрд╛ред
UICollectionViewFlowLayout
рдХрд╛ рдПрдХ рдЙрдкрд╡рд░реНрдЧ рдмрдирд╛рдПрдВ рдФрд░ рдЗрд╕рдХреЗ
layoutAttributesForElements(in:)
рдУрд╡рд░рд░рд╛рдЗрдб
layoutAttributesForElements(in:)
ред рд╡рд┐рдзрд┐
UICollectionViewLayoutAttributes
рдХреА рдПрдХ рд╕рд░рдгреА рджреЗрддрд╛ рд╣реИ, рдЬреЛ рдХрд┐рд╕реА рд╡рд┐рд╢реЗрд╖ рд╕реЗрд▓ рдХреЛ рдкреНрд░рджрд░реНрд╢рд┐рдд рдХрд░рдиреЗ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдЬрд╛рдирдХрд╛рд░реА рдкреНрд░рджрд╛рди рдХрд░рддрд╛ рд╣реИред
рд╕рдВрдЧреНрд░рд╣ рдЕрдиреБрд░реЛрдзреЛрдВ рдореЗрдВ рд╣рд░ рдмрд╛рд░
contentOffset
рдкрд░рд┐рд╡рд░реНрддрди рдХреА рд╡рд┐рд╢реЗрд╖рддрд╛ рд╣реЛрддреА рд╣реИ, рд╕рд╛рде рд╣реА рд▓реЗрдЖрдЙрдЯ рдЕрдорд╛рдиреНрдп рд╣реЛрдиреЗ рдкрд░ рднреАред рдЗрд╕рдХреЗ рдЕрд▓рд╛рд╡рд╛, рд╣рдо
parallaxValue
рд╕рдВрдкрддреНрддрд┐ рдХреЛ рдЬреЛрдбрд╝рдХрд░ рдХрд╕реНрдЯрдо рд╡рд┐рд╢реЗрд╖рддрд╛рдУрдВ рдХрд╛ рдирд┐рд░реНрдорд╛рдг рдХрд░реЗрдВрдЧреЗ, рдЬреЛ рдпрд╣ рдирд┐рд░реНрдзрд╛рд░рд┐рдд рдХрд░рддрд╛ рд╣реИ рдХрд┐ рдЪрд┐рддреНрд░ рдХрд╛ рдлреНрд░реЗрдо рд╕реЗрд▓ рдХреЗ рдлреНрд░реЗрдо рд╕реЗ рдХрд┐рддрдирд╛ рд╡рд┐рд▓рдВрдмрд┐рдд рд╣реИред рд╡рд┐рд╢реЗрд╖рддрд╛ рдЙрдкрд╡рд░реНрдЧреЛрдВ рдХреЗ рд▓рд┐рдП, рдЖрдкрдХреЛ рдЙрдирдХреЗ рд▓рд┐рдП
NSCopiyng
рдХреЛ рдУрд╡рд░рд░рд╛рдЗрдб рдХрд░рдирд╛ рд╣реЛрдЧрд╛ред рдПрдкреНрдкрд▓:
рдпрджрд┐ рдЖрдк рдХрд┐рд╕реА рднреА рдХрд╕реНрдЯрдо рд▓реЗрдЖрдЙрдЯ рд╡рд┐рд╢реЗрд╖рддрд╛рдУрдВ рдХреЛ рдЙрдк-рд╡рд░реНрдЧ рдХрд░рддреЗ рд╣реИрдВ рдФрд░ рдХрд╛рд░реНрдпрд╛рдиреНрд╡рд┐рдд рдХрд░рддреЗ рд╣реИрдВ, рддреЛ рдЖрдкрдХреЛ рдЕрдкрдиреЗ рдЧреБрдгреЛрдВ рдХреЗ рдореВрд▓реНрдпреЛрдВ рдХреА рддреБрд▓рдирд╛ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд╡рд┐рд░рд╛рд╕рдд рдореЗрдВ рдорд┐рд▓реА рд░рд╛рд╢рд┐ рдХреЛ рднреА рдУрд╡рд░рд░рд╛рдЗрдб рдХрд░рдирд╛ рдЪрд╛рд╣рд┐рдПред IOS 7 рдФрд░ рдмрд╛рдж рдореЗрдВ, рд╕рдВрдЧреНрд░рд╣ рджреГрд╢реНрдп рд▓реЗрдЖрдЙрдЯ рд╡рд┐рд╢реЗрд╖рддрд╛рдУрдВ рдХреЛ рд▓рд╛рдЧреВ рдирд╣реАрдВ рдХрд░рддрд╛ рд╣реИ рдпрджрд┐ рдЙрди рд╡рд┐рд╢реЗрд╖рддрд╛рдУрдВ рдХреЛ рдирд╣реАрдВ рдмрджрд▓рд╛ рд╣реИред рдпрд╣ рдирд┐рд░реНрдзрд╛рд░рд┐рдд рдХрд░рддрд╛ рд╣реИ рдХрд┐ рдХреНрдпрд╛ рдкреБрд░рд╛рдиреА рдФрд░ рдирдИ рд╡рд┐рд╢реЗрд╖рддрд╛ рд╡рд╕реНрддреБрдУрдВ рдХреА рддреБрд▓рдирд╛ рдЗрд╕реНрдХреНрд╡рд╛рд▓: рд╡рд┐рдзрд┐ рдХреЗ рдЙрдкрдпреЛрдЧ рд╕реЗ рдХреА рдЧрдИ рд╣реИ рдпрд╛ рдирд╣реАрдВред рдХреНрдпреЛрдВрдХрд┐ рдЗрд╕ рдкрджреНрдзрддрд┐ рдХрд╛ рдбрд┐рдлрд╝реЙрд▓реНрдЯ рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рдХреЗрд╡рд▓ рдЗрд╕ рд╡рд░реНрдЧ рдХреЗ рдореМрдЬреВрджрд╛ рдЧреБрдгреЛрдВ рдХреА рдЬрд╛рдВрдЪ рдХрд░рддрд╛ рд╣реИ, рдЗрд╕рд▓рд┐рдП рдЖрдкрдХреЛ рдХрд┐рд╕реА рднреА рдЕрддрд┐рд░рд┐рдХреНрдд рдЧреБрдгреЛрдВ рдХреА рддреБрд▓рдирд╛ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдЕрдкрдиреЗ рд╕реНрд╡рдпрдВ рдХреЗ рд╕рдВрд╕реНрдХрд░рдг рдХреЛ рд▓рд╛рдЧреВ рдХрд░рдирд╛ рд╣реЛрдЧрд╛ред рдпрджрд┐ рдЖрдкрдХреЗ рдХрд╕реНрдЯрдо рдЧреБрдг рд╕рднреА рд╕рдорд╛рди рд╣реИрдВ, рддреЛ super
рдХреЙрд▓ рдХрд░реЗрдВ рдФрд░ рдЕрдкрдиреЗ рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рдХреЗ рдЕрдВрдд рдореЗрдВ рдкрд░рд┐рдгрд╛рдореА рдорд╛рди рд▓реМрдЯрд╛рдПрдВред
рдХреИрд╕реЗ рдкрддрд╛ рдХрд░реЗрдВ
parallaxValue
? рдЖрдЗрдП рдЧрдгрдирд╛ рдХрд░реЗрдВ рдХрд┐ рдЖрдкрдХреЛ рд╕реЗрд▓ рдХреЗ рдХреЗрдВрджреНрд░ рдХреЛ рд╕реНрдерд╛рдирд╛рдВрддрд░рд┐рдд рдХрд░рдиреЗ рдХреА рдХрд┐рддрдиреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ рддрд╛рдХрд┐ рдпрд╣ рдХреЗрдВрджреНрд░ рдореЗрдВ рдЦрдбрд╝рд╛ рд╣реЛред рдпрджрд┐ рдпрд╣ рджреВрд░реА рд╕реЗрд▓ рдХреА рдЪреМрдбрд╝рд╛рдИ рд╕реЗ рдЕрдзрд┐рдХ рд╣реИ, рддреЛ рдЙрд╕ рдкрд░ рд╣рдереМрдбрд╝рд╛ рдХрд░реЗрдВред рдЕрдиреНрдпрдерд╛, рдЗрд╕ рджреВрд░реА рдХреЛ
рд╕реЗрд▓ рдХреА рдЪреМрдбрд╝рд╛рдИ рд╕реЗ рд╡рд┐рднрд╛рдЬрд┐рдд рдХрд░реЗрдВред рдпрд╣ рджреВрд░реА рд╢реВрдиреНрдп рдХреЗ рдХрд░реАрдм рд╣реИ, рдХрдордЬреЛрд░ рд▓рдВрдмрди рдкреНрд░рднрд╛рд╡ред
class ParallaxLayoutAttributes: UICollectionViewLayoutAttributes { var parallaxValue: CGFloat? }тАЛ class PreviewLayout: UICollectionViewFlowLayout { var offsetBetweenCells: CGFloat = 44тАЛ override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true }тАЛ override class var layoutAttributesClass: AnyClass { return ParallaxLayoutAttributes.self }тАЛ override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return super.layoutAttributesForElements(in: rect)? .compactMap { $0.copy() as? ParallaxLayoutAttributes } .compactMap(prepareAttributes) }тАЛ private func prepareAttributes(attributes: ParallaxLayoutAttributes) -> ParallaxLayoutAttributes { guard let collectionView = self.collectionView else { return attributes }тАЛ let width = itemSize.width let centerX = width / 2 let distanceToCenter = attributes.center.x - collectionView.contentOffset.x let relativeDistanceToCenter = (distanceToCenter - centerX) / widthтАЛ if abs(relativeDistanceToCenter) >= 1 { attributes.parallaxValue = nil attributes.transform = .identity } else { attributes.parallaxValue = relativeDistanceToCenter attributes.transform = CGAffineTransform(translationX: relativeDistanceToCenter * offsetBetweenCells, y: 0) } return attributes } }
class ParallaxLayoutAttributes: UICollectionViewLayoutAttributes { var parallaxValue: CGFloat? }тАЛ class PreviewLayout: UICollectionViewFlowLayout { var offsetBetweenCells: CGFloat = 44тАЛ override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true }тАЛ override class var layoutAttributesClass: AnyClass { return ParallaxLayoutAttributes.self }тАЛ override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return super.layoutAttributesForElements(in: rect)? .compactMap { $0.copy() as? ParallaxLayoutAttributes } .compactMap(prepareAttributes) }тАЛ private func prepareAttributes(attributes: ParallaxLayoutAttributes) -> ParallaxLayoutAttributes { guard let collectionView = self.collectionView else { return attributes }тАЛ let width = itemSize.width let centerX = width / 2 let distanceToCenter = attributes.center.x - collectionView.contentOffset.x let relativeDistanceToCenter = (distanceToCenter - centerX) / widthтАЛ if abs(relativeDistanceToCenter) >= 1 { attributes.parallaxValue = nil attributes.transform = .identity } else { attributes.parallaxValue = relativeDistanceToCenter attributes.transform = CGAffineTransform(translationX: relativeDistanceToCenter * offsetBetweenCells, y: 0) } return attributes } }
class ParallaxLayoutAttributes: UICollectionViewLayoutAttributes { var parallaxValue: CGFloat? }тАЛ class PreviewLayout: UICollectionViewFlowLayout { var offsetBetweenCells: CGFloat = 44тАЛ override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true }тАЛ override class var layoutAttributesClass: AnyClass { return ParallaxLayoutAttributes.self }тАЛ override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return super.layoutAttributesForElements(in: rect)? .compactMap { $0.copy() as? ParallaxLayoutAttributes } .compactMap(prepareAttributes) }тАЛ private func prepareAttributes(attributes: ParallaxLayoutAttributes) -> ParallaxLayoutAttributes { guard let collectionView = self.collectionView else { return attributes }тАЛ let width = itemSize.width let centerX = width / 2 let distanceToCenter = attributes.center.x - collectionView.contentOffset.x let relativeDistanceToCenter = (distanceToCenter - centerX) / widthтАЛ if abs(relativeDistanceToCenter) >= 1 { attributes.parallaxValue = nil attributes.transform = .identity } else { attributes.parallaxValue = relativeDistanceToCenter attributes.transform = CGAffineTransform(translationX: relativeDistanceToCenter * offsetBetweenCells, y: 0) } return attributes } }
class ParallaxLayoutAttributes: UICollectionViewLayoutAttributes { var parallaxValue: CGFloat? }тАЛ class PreviewLayout: UICollectionViewFlowLayout { var offsetBetweenCells: CGFloat = 44тАЛ override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true }тАЛ override class var layoutAttributesClass: AnyClass { return ParallaxLayoutAttributes.self }тАЛ override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return super.layoutAttributesForElements(in: rect)? .compactMap { $0.copy() as? ParallaxLayoutAttributes } .compactMap(prepareAttributes) }тАЛ private func prepareAttributes(attributes: ParallaxLayoutAttributes) -> ParallaxLayoutAttributes { guard let collectionView = self.collectionView else { return attributes }тАЛ let width = itemSize.width let centerX = width / 2 let distanceToCenter = attributes.center.x - collectionView.contentOffset.x let relativeDistanceToCenter = (distanceToCenter - centerX) / widthтАЛ if abs(relativeDistanceToCenter) >= 1 { attributes.parallaxValue = nil attributes.transform = .identity } else { attributes.parallaxValue = relativeDistanceToCenter attributes.transform = CGAffineTransform(translationX: relativeDistanceToCenter * offsetBetweenCells, y: 0) } return attributes } }
class ParallaxLayoutAttributes: UICollectionViewLayoutAttributes { var parallaxValue: CGFloat? }тАЛ class PreviewLayout: UICollectionViewFlowLayout { var offsetBetweenCells: CGFloat = 44тАЛ override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true }тАЛ override class var layoutAttributesClass: AnyClass { return ParallaxLayoutAttributes.self }тАЛ override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return super.layoutAttributesForElements(in: rect)? .compactMap { $0.copy() as? ParallaxLayoutAttributes } .compactMap(prepareAttributes) }тАЛ private func prepareAttributes(attributes: ParallaxLayoutAttributes) -> ParallaxLayoutAttributes { guard let collectionView = self.collectionView else { return attributes }тАЛ let width = itemSize.width let centerX = width / 2 let distanceToCenter = attributes.center.x - collectionView.contentOffset.x let relativeDistanceToCenter = (distanceToCenter - centerX) / widthтАЛ if abs(relativeDistanceToCenter) >= 1 { attributes.parallaxValue = nil attributes.transform = .identity } else { attributes.parallaxValue = relativeDistanceToCenter attributes.transform = CGAffineTransform(translationX: relativeDistanceToCenter * offsetBetweenCells, y: 0) } return attributes } }
class ParallaxLayoutAttributes: UICollectionViewLayoutAttributes { var parallaxValue: CGFloat? }тАЛ class PreviewLayout: UICollectionViewFlowLayout { var offsetBetweenCells: CGFloat = 44тАЛ override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true }тАЛ override class var layoutAttributesClass: AnyClass { return ParallaxLayoutAttributes.self }тАЛ override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return super.layoutAttributesForElements(in: rect)? .compactMap { $0.copy() as? ParallaxLayoutAttributes } .compactMap(prepareAttributes) }тАЛ private func prepareAttributes(attributes: ParallaxLayoutAttributes) -> ParallaxLayoutAttributes { guard let collectionView = self.collectionView else { return attributes }тАЛ let width = itemSize.width let centerX = width / 2 let distanceToCenter = attributes.center.x - collectionView.contentOffset.x let relativeDistanceToCenter = (distanceToCenter - centerX) / widthтАЛ if abs(relativeDistanceToCenter) >= 1 { attributes.parallaxValue = nil attributes.transform = .identity } else { attributes.parallaxValue = relativeDistanceToCenter attributes.transform = CGAffineTransform(translationX: relativeDistanceToCenter * offsetBetweenCells, y: 0) } return attributes } }
class ParallaxLayoutAttributes: UICollectionViewLayoutAttributes { var parallaxValue: CGFloat? }тАЛ class PreviewLayout: UICollectionViewFlowLayout { var offsetBetweenCells: CGFloat = 44тАЛ override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true }тАЛ override class var layoutAttributesClass: AnyClass { return ParallaxLayoutAttributes.self }тАЛ override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return super.layoutAttributesForElements(in: rect)? .compactMap { $0.copy() as? ParallaxLayoutAttributes } .compactMap(prepareAttributes) }тАЛ private func prepareAttributes(attributes: ParallaxLayoutAttributes) -> ParallaxLayoutAttributes { guard let collectionView = self.collectionView else { return attributes }тАЛ let width = itemSize.width let centerX = width / 2 let distanceToCenter = attributes.center.x - collectionView.contentOffset.x let relativeDistanceToCenter = (distanceToCenter - centerX) / widthтАЛ if abs(relativeDistanceToCenter) >= 1 { attributes.parallaxValue = nil attributes.transform = .identity } else { attributes.parallaxValue = relativeDistanceToCenter attributes.transform = CGAffineTransform(translationX: relativeDistanceToCenter * offsetBetweenCells, y: 0) } return attributes } }
class ParallaxLayoutAttributes: UICollectionViewLayoutAttributes { var parallaxValue: CGFloat? }тАЛ class PreviewLayout: UICollectionViewFlowLayout { var offsetBetweenCells: CGFloat = 44тАЛ override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true }тАЛ override class var layoutAttributesClass: AnyClass { return ParallaxLayoutAttributes.self }тАЛ override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return super.layoutAttributesForElements(in: rect)? .compactMap { $0.copy() as? ParallaxLayoutAttributes } .compactMap(prepareAttributes) }тАЛ private func prepareAttributes(attributes: ParallaxLayoutAttributes) -> ParallaxLayoutAttributes { guard let collectionView = self.collectionView else { return attributes }тАЛ let width = itemSize.width let centerX = width / 2 let distanceToCenter = attributes.center.x - collectionView.contentOffset.x let relativeDistanceToCenter = (distanceToCenter - centerX) / widthтАЛ if abs(relativeDistanceToCenter) >= 1 { attributes.parallaxValue = nil attributes.transform = .identity } else { attributes.parallaxValue = relativeDistanceToCenter attributes.transform = CGAffineTransform(translationX: relativeDistanceToCenter * offsetBetweenCells, y: 0) } return attributes } }



рдЬрдм рд╕рдВрдЧреНрд░рд╣ рдЖрд╡рд╢реНрдпрдХ рдЧреБрдг рдкреНрд░рд╛рдкреНрдд рдХрд░рддрд╛ рд╣реИ, рддреЛ рдХреЛрд╢рд┐рдХрд╛рдПрдВ рдЙрдиреНрд╣реЗрдВ
рд▓рд╛рдЧреВ рдХрд░рддреА рд╣реИрдВред рдЗрд╕ рд╡реНрдпрд╡рд╣рд╛рд░ рдХреЛ рд╕реЗрд▓ рдХреЗ рдЙрдкрд╡рд░реНрдЧ рдореЗрдВ рдУрд╡рд░рд░рд╛рдЗрдб рдХрд┐рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИред рдЖрдЗрдП
parallaxValue
рдЖрдзрд╛рд░ рдкрд░
imageView
ред рд╣рд╛рд▓рд╛рдБрдХрд┐,
contentMode == .aspectFit
рд╕рд╛рде рдЪрд┐рддреНрд░реЛрдВ рдХреЛ рд╕рд╣реА рддрд░реАрдХреЗ рд╕реЗ рдХрд╛рдо рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рдпрд╣ рдкрд░реНрдпрд╛рдкреНрдд рдирд╣реАрдВ рд╣реИ, рдХреНрдпреЛрдВрдХрд┐ рдЪрд┐рддреНрд░ рдХрд╛ рдлреНрд░реЗрдо
imageView
рдлреНрд░реЗрдо рдХреЗ рд╕рд╛рде рдореЗрд▓ рдирд╣реАрдВ рдЦрд╛рддрд╛ рд╣реИ, рдЬрд┐рд╕рдХреЗ рджреНрд╡рд╛рд░рд╛
imageView
рд╕рд╛рде рд╕рд╛рдордЧреНрд░реА рдХреНрд░реЙрдк рдХреА рдЬрд╛рддреА
clipsToBounds == true
ред
clipsToBounds == true
ред рдЙрдЪрд┐рдд
contentMode
рд╕рд╛рде рдЫрд╡рд┐ рдХреЗ рдЖрдХрд╛рд░ рд╕реЗ рдореЗрд▓ рдЦрд╛рддрд╛ рдорд╛рд╕реНрдХ
contentMode
рдФрд░ рдпрджрд┐ рдЖрд╡рд╢реНрдпрдХ рд╣реЛ рддреЛ рд╣рдо рдЗрд╕реЗ рдЕрдкрдбреЗрдЯ рдХрд░реЗрдВрдЧреЗред рдЕрдм рд╕рдм рдХреБрдЫ рдХрд╛рдо рдХрд░рддрд╛ рд╣реИ!
extension PreviewCollectionViewCell {тАЛ override func layoutSubviews() {тАЛ super.layoutSubviews() guard let imageSize = imageView.image?.size else { return } let imageRect = AVMakeRect(aspectRatio: imageSize, insideRect: bounds)тАЛ let path = UIBezierPath(rect: imageRect) let shapeLayer = CAShapeLayer() shapeLayer.path = path.cgPath layer.mask = shapeLayer } override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {тАЛ guard let attrs = layoutAttributes as? ParallaxLayoutAttributes else { return super.apply(layoutAttributes) } let parallaxValue = attrs.parallaxValue ?? 0 let transition = -(bounds.width * 0.3 * parallaxValue) imageView.transform = CGAffineTransform(translationX: transition, y: .zero) } }
extension PreviewCollectionViewCell {тАЛ override func layoutSubviews() {тАЛ super.layoutSubviews() guard let imageSize = imageView.image?.size else { return } let imageRect = AVMakeRect(aspectRatio: imageSize, insideRect: bounds)тАЛ let path = UIBezierPath(rect: imageRect) let shapeLayer = CAShapeLayer() shapeLayer.path = path.cgPath layer.mask = shapeLayer } override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {тАЛ guard let attrs = layoutAttributes as? ParallaxLayoutAttributes else { return super.apply(layoutAttributes) } let parallaxValue = attrs.parallaxValue ?? 0 let transition = -(bounds.width * 0.3 * parallaxValue) imageView.transform = CGAffineTransform(translationX: transition, y: .zero) } }
extension PreviewCollectionViewCell {тАЛ override func layoutSubviews() {тАЛ super.layoutSubviews() guard let imageSize = imageView.image?.size else { return } let imageRect = AVMakeRect(aspectRatio: imageSize, insideRect: bounds)тАЛ let path = UIBezierPath(rect: imageRect) let shapeLayer = CAShapeLayer() shapeLayer.path = path.cgPath layer.mask = shapeLayer } override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {тАЛ guard let attrs = layoutAttributes as? ParallaxLayoutAttributes else { return super.apply(layoutAttributes) } let parallaxValue = attrs.parallaxValue ?? 0 let transition = -(bounds.width * 0.3 * parallaxValue) imageView.transform = CGAffineTransform(translationX: transition, y: .zero) } }
extension PreviewCollectionViewCell {тАЛ override func layoutSubviews() {тАЛ super.layoutSubviews() guard let imageSize = imageView.image?.size else { return } let imageRect = AVMakeRect(aspectRatio: imageSize, insideRect: bounds)тАЛ let path = UIBezierPath(rect: imageRect) let shapeLayer = CAShapeLayer() shapeLayer.path = path.cgPath layer.mask = shapeLayer } override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {тАЛ guard let attrs = layoutAttributes as? ParallaxLayoutAttributes else { return super.apply(layoutAttributes) } let parallaxValue = attrs.parallaxValue ?? 0 let transition = -(bounds.width * 0.3 * parallaxValue) imageView.transform = CGAffineTransform(translationX: transition, y: .zero) } }
extension PreviewCollectionViewCell {тАЛ override func layoutSubviews() {тАЛ super.layoutSubviews() guard let imageSize = imageView.image?.size else { return } let imageRect = AVMakeRect(aspectRatio: imageSize, insideRect: bounds)тАЛ let path = UIBezierPath(rect: imageRect) let shapeLayer = CAShapeLayer() shapeLayer.path = path.cgPath layer.mask = shapeLayer } override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {тАЛ guard let attrs = layoutAttributes as? ParallaxLayoutAttributes else { return super.apply(layoutAttributes) } let parallaxValue = attrs.parallaxValue ?? 0 let transition = -(bounds.width * 0.3 * parallaxValue) imageView.transform = CGAffineTransform(translationX: transition, y: .zero) } }

2. рдПрдХ рдЫреЛрдЯреЗ рд╕рдВрдЧреНрд░рд╣ рдХреЗ рддрддреНрд╡ рдХреЗрдВрджреНрд░рд┐рдд рд╣реИрдВ

рдпрд╣рд╛рдВ рд╕рдм рдХреБрдЫ рдмрд╣реБрдд рд╕рд░рд▓ рд╣реИред рдпрд╣ рдкреНрд░рднрд╛рд╡ рдмрд╛рдПрдБ рдФрд░ рджрд╛рдПрдБ рджреЛрдиреЛрдВ рдкрд░ рдмрдбрд╝реЗ
inset
рд╕реЗрдЯ рдХрд░рдХреЗ рдкреНрд░рд╛рдкреНрдд рдХрд┐рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИред рджрд╛рдПрдВ / рдмрд╛рдПрдВ рд╕реНрдХреНрд░реЙрд▓ рдХрд░рддреЗ рд╕рдордп, рдХреЗрд╡рд▓ рддрднреА
bouncing
рд╢реБрд░реВ рдХрд░рдирд╛ рдЖрд╡рд╢реНрдпрдХ рд╣реИ рдЬрдм рдЕрдВрддрд┐рдо рд╕реЗрд▓ рдиреЗ рджреГрд╢реНрдп рд╕рд╛рдордЧреНрд░реА рдХреЛ рдЫреЛрдбрд╝ рджрд┐рдпрд╛ рд╣реЛред рдпрд╣реА рд╣реИ, рджреГрд╢реНрдпрдорд╛рди рд╕рд╛рдордЧреНрд░реА рд╕реЗрд▓ рдХреЗ рдЖрдХрд╛рд░ рдХреЗ рдмрд░рд╛рдмрд░ рд╣реЛрдиреА рдЪрд╛рд╣рд┐рдПред
extension ThumbnailFlowLayout {тАЛ var farInset: CGFloat { guard let collection = collectionView else { return .zero } return (collection.bounds.width - itemSize.width) / 2 } var insets: UIEdgeInsets { UIEdgeInsets(top: .zero, left: farInset, bottom: .zero, right: farInset) }тАЛ override func prepare() { collectionView?.contentInset = insets super.prepare() } }
extension ThumbnailFlowLayout {тАЛ var farInset: CGFloat { guard let collection = collectionView else { return .zero } return (collection.bounds.width - itemSize.width) / 2 } var insets: UIEdgeInsets { UIEdgeInsets(top: .zero, left: farInset, bottom: .zero, right: farInset) }тАЛ override func prepare() { collectionView?.contentInset = insets super.prepare() } }
extension ThumbnailFlowLayout {тАЛ var farInset: CGFloat { guard let collection = collectionView else { return .zero } return (collection.bounds.width - itemSize.width) / 2 } var insets: UIEdgeInsets { UIEdgeInsets(top: .zero, left: farInset, bottom: .zero, right: farInset) }тАЛ override func prepare() { collectionView?.contentInset = insets super.prepare() } }



рдХреЗрдВрджреНрд░рд┐рдд рдХрд░рдиреЗ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдЕрдзрд┐рдХ: рдЬрдм рд╕рдВрдЧреНрд░рд╣ рд╕реНрдХреНрд░реЙрд▓ рдХрд░рдирд╛ рд╕рдорд╛рдкреНрдд рдХрд░ рджреЗрддрд╛ рд╣реИ, рддреЛ рд▓реЗрдЖрдЙрдЯ
contentOffset
рдХреЛ рд░реЛрдХрдиреЗ рдХрд╛ рдЕрдиреБрд░реЛрдз рдХрд░рддрд╛ рд╣реИред рдРрд╕рд╛ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рдУрд╡рд░рд░рд╛рдЗрдб
targetContentOffset(forProposedContentOffset:withScrollingVelocity:)
ред рдПрдкреНрдкрд▓:
рдпрджрд┐ рдЖрдк рд╕реНрдХреНрд░реЙрд▓рд┐рдВрдЧ рд╡реНрдпрд╡рд╣рд╛рд░ рдХреЛ рд╡рд┐рд╢рд┐рд╖реНрдЯ рд╕реАрдорд╛рдУрдВ рдХреЗ рд▓рд┐рдП рд╕реНрдиреИрдк рдХрд░рдирд╛ рдЪрд╛рд╣рддреЗ рд╣реИрдВ, рддреЛ рдЖрдк рдЗрд╕ рдкрджреНрдзрддрд┐ рдХреЛ рдУрд╡рд░рд░рд╛рдЗрдб рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ рдФрд░ рдЗрд╕рдХрд╛ рдЙрдкрдпреЛрдЧ рдЙрд╕ рдмрд┐рдВрджреБ рдХреЛ рдмрджрд▓рдиреЗ рдХреЗ рд▓рд┐рдП рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ рдЬрд┐рд╕ рдкрд░ рд░реЛрдХрдирд╛ рд╣реИред рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП, рдЖрдк рдЖрдЗрдЯрдо рдХреЗ рдмреАрдЪ рд╕реАрдорд╛ рдкрд░ рд╕реНрдХреНрд░реЙрд▓ рдХреЛ рд░реЛрдХрдиреЗ рдХреЗ рд▓рд┐рдП рд╣рдореЗрд╢рд╛ рдЗрд╕ рдкрджреНрдзрддрд┐ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ, рдЬреИрд╕рд╛ рдХрд┐ рдХрд┐рд╕реА рдЖрдЗрдЯрдо рдХреЗ рдмреАрдЪ рдореЗрдВ рд░реБрдХрдиреЗ рдХреЗ рд▓рд┐рдП рд╣реЛрддрд╛ рд╣реИред
рд╕рдм рдХреБрдЫ рд╕реБрдВрджрд░ рдмрдирд╛рдиреЗ рдХреЗ рд▓рд┐рдП, рд╣рдо
рд╣рдореЗрд╢рд╛ рдирд┐рдХрдЯрддрдо рд╕реЗрд▓ рдХреЗ рдХреЗрдВрджреНрд░ рдореЗрдВ рд░реБрдХреЗрдВрдЧреЗред рдирд┐рдХрдЯрддрдо рд╕реЗрд▓ рдХреЗ рдХреЗрдВрджреНрд░ рдХреА рдЧрдгрдирд╛ рдХрд░рдирд╛ рдПрдХ рддреБрдЪреНрдЫ рдХрд╛рд░реНрдп рд╣реИ, рд▓реЗрдХрд┐рди рдЖрдкрдХреЛ рд╕рд╛рд╡рдзрд╛рди рд░рд╣рдиреЗ рдФрд░ рд╕рд╛рдордЧреНрд░реА рдкрд░ рд╡рд┐рдЪрд╛рд░ рдХрд░рдиреЗ рдХреА
contentInset
ред
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { guard let collection = collectionView else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity) } let cellWithSpacing = itemSize.width + config.distanceBetween let relative = (proposedContentOffset.x + collection.contentInset.left) / cellWithSpacing let leftIndex = max(0, floor(relative)) let rightIndex = min(ceil(relative), CGFloat(itemsCount)) let leftCenter = leftIndex * cellWithSpacing - collection.contentInset.left let rightCenter = rightIndex * cellWithSpacing - collection.contentInset.leftтАЛ if abs(leftCenter - proposedContentOffset.x) < abs(rightCenter - proposedContentOffset.x) { return CGPoint(x: leftCenter, y: proposedContentOffset.y) } else { return CGPoint(x: rightCenter, y: proposedContentOffset.y) } }
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { guard let collection = collectionView else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity) } let cellWithSpacing = itemSize.width + config.distanceBetween let relative = (proposedContentOffset.x + collection.contentInset.left) / cellWithSpacing let leftIndex = max(0, floor(relative)) let rightIndex = min(ceil(relative), CGFloat(itemsCount)) let leftCenter = leftIndex * cellWithSpacing - collection.contentInset.left let rightCenter = rightIndex * cellWithSpacing - collection.contentInset.leftтАЛ if abs(leftCenter - proposedContentOffset.x) < abs(rightCenter - proposedContentOffset.x) { return CGPoint(x: leftCenter, y: proposedContentOffset.y) } else { return CGPoint(x: rightCenter, y: proposedContentOffset.y) } }


3. рдПрдХ рдЫреЛрдЯреЗ рд╕рдВрдЧреНрд░рд╣ рдХреЗ рддрддреНрд╡реЛрдВ рдХрд╛ рдЧрддрд┐рд╢реАрд▓ рдЖрдХрд╛рд░
рдпрджрд┐ рдЖрдк рдПрдХ рдмрдбрд╝реЗ рд╕рдВрдЧреНрд░рд╣ рдХреЛ рд╕реНрдХреНрд░реЙрд▓ рдХрд░рддреЗ рд╣реИрдВ, рддреЛ
contentOffset
рдПрдХ рдЫреЛрдЯреЗ рд╕реЗ рдПрдХ рдХреЗ рд▓рд┐рдП
contentOffset
ред рдЗрд╕рдХреЗ рдЕрд▓рд╛рд╡рд╛, рдПрдХ рдЫреЛрдЯреЗ рд╕рдВрдЧреНрд░рд╣ рдХрд╛ рдХреЗрдВрджреНрд░реАрдп рд╕реЗрд▓ рдмрд╛рдХреА рдХреЗ рд░реВрдк рдореЗрдВ рдмрдбрд╝рд╛ рдирд╣реАрдВ рд╣реИред рд╕рд╛рдЗрдб рдХреЛрд╢рд┐рдХрд╛рдУрдВ рдХрд╛ рдПрдХ рдирд┐рд╢реНрдЪрд┐рдд рдЖрдХрд╛рд░ рд╣реЛрддрд╛ рд╣реИ, рдФрд░ рдХреЗрдВрджреНрд░реАрдп рдПрдХ рддрд╕реНрд╡реАрд░ рдХреЗ рдкрд╣рд▓реВ рдЕрдиреБрдкрд╛рдд рдХреЗ рд╕рд╛рде рдореЗрд▓ рдЦрд╛рддрд╛ рд╣реИ рдЬрд┐рд╕рдореЗрдВ рдпрд╣ рд╢рд╛рдорд┐рд▓ рд╣реИред

рдЖрдк рдЙрд╕реА рддрдХрдиреАрдХ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ рдЬреИрд╕реЗ рдХрд┐ рд▓рдВрдмрди рдХреЗ рдорд╛рдорд▓реЗ рдореЗрдВред рд╣рдо рдПрдХ рдЫреЛрдЯреЗ рд╕рдВрдЧреНрд░рд╣ рдХреЗ рд▓рд┐рдП рдПрдХ рдХрд╕реНрдЯрдо
UICollectionViewFlowLayout
рдФрд░
prepareAttributes(attributes:
рдлрд┐рд░ рд╕реЗ рдкрд░рд┐рднрд╛рд╖рд┐рдд
prepareAttributes(attributes:
рдпрд╣ рджреЗрдЦрддреЗ рд╣реБрдП рдХрд┐ рдЫреЛрдЯреЗ рд╕рдВрдЧреНрд░рд╣ рдХрд╛ рд▓реЗрдЖрдЙрдЯ рддрд░реНрдХ рдЖрдЧреЗ рдЬрдЯрд┐рд▓ рд╣реЛрдЧрд╛, рд╣рдо рд╕реЗрд▓ рдЬреНрдпрд╛рдорд┐рддрд┐ рдХреЗ рднрдВрдбрд╛рд░рдг рдФрд░ рдЧрдгрдирд╛ рдХреЗ рд▓рд┐рдП рдПрдХ рдЕрд▓рдЧ рдЗрдХрд╛рдИ рдмрдирд╛рдПрдВрдЧреЗред)
struct Cell { let indexPath: IndexPathтАЛ let dims: Dimensions let state: StateтАЛ func updated(new state: State) -> Cell { return Cell(indexPath: indexPath, dims: dims, state: state) } }тАЛ extension Cell { struct Dimensions {тАЛ let defaultSize: CGSize let aspectRatio: CGFloat let inset: CGFloat let insetAsExpanded: CGFloat }тАЛ struct State {тАЛ let expanding: CGFloatтАЛ static var `default`: State { State(expanding: .zero) } } }
struct Cell { let indexPath: IndexPathтАЛ let dims: Dimensions let state: StateтАЛ func updated(new state: State) -> Cell { return Cell(indexPath: indexPath, dims: dims, state: state) } }тАЛ extension Cell { struct Dimensions {тАЛ let defaultSize: CGSize let aspectRatio: CGFloat let inset: CGFloat let insetAsExpanded: CGFloat }тАЛ struct State {тАЛ let expanding: CGFloatтАЛ static var `default`: State { State(expanding: .zero) } } }
struct Cell { let indexPath: IndexPathтАЛ let dims: Dimensions let state: StateтАЛ func updated(new state: State) -> Cell { return Cell(indexPath: indexPath, dims: dims, state: state) } }тАЛ extension Cell { struct Dimensions {тАЛ let defaultSize: CGSize let aspectRatio: CGFloat let inset: CGFloat let insetAsExpanded: CGFloat }тАЛ struct State {тАЛ let expanding: CGFloatтАЛ static var `default`: State { State(expanding: .zero) } } }
struct Cell { let indexPath: IndexPathтАЛ let dims: Dimensions let state: StateтАЛ func updated(new state: State) -> Cell { return Cell(indexPath: indexPath, dims: dims, state: state) } }тАЛ extension Cell { struct Dimensions {тАЛ let defaultSize: CGSize let aspectRatio: CGFloat let inset: CGFloat let insetAsExpanded: CGFloat }тАЛ struct State {тАЛ let expanding: CGFloatтАЛ static var `default`: State { State(expanding: .zero) } } }
struct Cell { let indexPath: IndexPathтАЛ let dims: Dimensions let state: StateтАЛ func updated(new state: State) -> Cell { return Cell(indexPath: indexPath, dims: dims, state: state) } }тАЛ extension Cell { struct Dimensions {тАЛ let defaultSize: CGSize let aspectRatio: CGFloat let inset: CGFloat let insetAsExpanded: CGFloat }тАЛ struct State {тАЛ let expanding: CGFloatтАЛ static var `default`: State { State(expanding: .zero) } } }
struct Cell { let indexPath: IndexPathтАЛ let dims: Dimensions let state: StateтАЛ func updated(new state: State) -> Cell { return Cell(indexPath: indexPath, dims: dims, state: state) } }тАЛ extension Cell { struct Dimensions {тАЛ let defaultSize: CGSize let aspectRatio: CGFloat let inset: CGFloat let insetAsExpanded: CGFloat }тАЛ struct State {тАЛ let expanding: CGFloatтАЛ static var `default`: State { State(expanding: .zero) } } }
struct Cell { let indexPath: IndexPathтАЛ let dims: Dimensions let state: StateтАЛ func updated(new state: State) -> Cell { return Cell(indexPath: indexPath, dims: dims, state: state) } }тАЛ extension Cell { struct Dimensions {тАЛ let defaultSize: CGSize let aspectRatio: CGFloat let inset: CGFloat let insetAsExpanded: CGFloat }тАЛ struct State {тАЛ let expanding: CGFloatтАЛ static var `default`: State { State(expanding: .zero) } } }
struct Cell { let indexPath: IndexPathтАЛ let dims: Dimensions let state: StateтАЛ func updated(new state: State) -> Cell { return Cell(indexPath: indexPath, dims: dims, state: state) } }тАЛ extension Cell { struct Dimensions {тАЛ let defaultSize: CGSize let aspectRatio: CGFloat let inset: CGFloat let insetAsExpanded: CGFloat }тАЛ struct State {тАЛ let expanding: CGFloatтАЛ static var `default`: State { State(expanding: .zero) } } }

UICollectionViewFlowLayout
рдкрд╛рд╕
collectionViewContentSize
UICollectionViewFlowLayout
рд╕рдВрдкрддреНрддрд┐ рд╣реИ рдЬреЛ рдЙрд╕ рдХреНрд╖реЗрддреНрд░ рдХреЗ рдЖрдХрд╛рд░ рдХреЛ рдирд┐рд░реНрдзрд╛рд░рд┐рдд рдХрд░рддреА рд╣реИ рдЬрд┐рд╕реЗ рд╕реНрдХреНрд░реЙрд▓ рдХрд┐рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИред рд╣рдорд╛рд░реЗ рдЬреАрд╡рди рдХреЛ рдЬрдЯрд┐рд▓ рдирд╣реАрдВ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рд╣рдореЗрдВ рдЗрд╕реЗ рдХреЗрдВрджреНрд░реАрдп рд╕реЗрд▓ рдХреЗ рдЖрдХрд╛рд░ рд╕реЗ рд╕реНрд╡рддрдВрддреНрд░ рдЫреЛрдбрд╝ рджреЗрдирд╛ рдЪрд╛рд╣рд┐рдПред рдкреНрд░рддреНрдпреЗрдХ рд╕реЗрд▓ рдХреЗ рд▓рд┐рдП рд╕рд╣реА рдЬреНрдпрд╛рдорд┐рддрд┐ рдХреЗ рд▓рд┐рдП, рдЖрдкрдХреЛ рдЪрд┐рддреНрд░ рдХрд╛
aspectRatio
рдФрд░
aspectRatio
рд╕реЗрд▓ рдХреЗ рдХреЗрдВрджреНрд░ рдХреА
aspectRatio
рдЬрд╛рдирдиреЗ рдХреА рдЬрд░реВрд░рдд рд╣реИред рд╕реЗрд▓ рдЬрд┐рддрдирд╛ рдХрд░реАрдм рд╣реЛрдЧрд╛,
size.width / size.height
рдЙрд╕рдХрд╛
size.width / size.height
рд╕реЗ
aspectRatio
ред рдПрдХ рд╡рд┐рд╢рд┐рд╖реНрдЯ рд╕реЗрд▓ рдХрд╛ рдЖрдХрд╛рд░
affineTransform
рдХрд░рддреЗ рд╕рдордп,
affineTransform
рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рд╢реЗрд╖ рдХреЛрд╢рд┐рдХрд╛рдУрдВ (рдЗрд╕рдХреЗ рджрд╛рдИрдВ рдФрд░ рдмрд╛рдИрдВ рдУрд░) рдХреЛ
affineTransform
ред рдпрд╣ рдкрддрд╛ рдЪрд▓рд╛ рд╣реИ рдХрд┐ рдХрд┐рд╕реА рд╡рд┐рд╢реЗрд╖ рд╕реЗрд▓ рдХреА рдЬреНрдпрд╛рдорд┐рддрд┐ рдХреА рдЧрдгрдирд╛ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рдЖрдкрдХреЛ рдкрдбрд╝реЛрд╕рд┐рдпреЛрдВ (рджреГрд╢реНрдпрдорд╛рди) рдХреА рд╡рд┐рд╢реЗрд╖рддрд╛рдУрдВ рдХреЛ рдЬрд╛рдирдирд╛ рд╣реЛрдЧрд╛ред
extension Cell {тАЛ func attributes(from layout: ThumbnailLayout, with sideCells: [Cell]) -> UICollectionViewLayoutAttributes? {тАЛ let attributes = layout.layoutAttributesForItem(at: indexPath)тАЛ attributes?.size = size attributes?.center = centerтАЛ let translate = sideCells.reduce(0) { (current, cell) -> CGFloat in if indexPath < cell.indexPath { return current - cell.additionalWidth / 2 } if indexPath > cell.indexPath { return current + cell.additionalWidth / 2 } return current } attributes?.transform = CGAffineTransform(translationX: translate, y: .zero)тАЛ return attributes } var additionalWidth: CGFloat { (dims.defaultSize.height * dims.aspectRatio - dims.defaultSize.width) * state.expanding } var size: CGSize { CGSize(width: dims.defaultSize.width + additionalWidth, height: dims.defaultSize.height) } var center: CGPoint { CGPoint(x: CGFloat(indexPath.row) * (dims.defaultSize.width + dims.inset) + dims.defaultSize.width / 2, y: dims.defaultSize.height / 2) } }
extension Cell {тАЛ func attributes(from layout: ThumbnailLayout, with sideCells: [Cell]) -> UICollectionViewLayoutAttributes? {тАЛ let attributes = layout.layoutAttributesForItem(at: indexPath)тАЛ attributes?.size = size attributes?.center = centerтАЛ let translate = sideCells.reduce(0) { (current, cell) -> CGFloat in if indexPath < cell.indexPath { return current - cell.additionalWidth / 2 } if indexPath > cell.indexPath { return current + cell.additionalWidth / 2 } return current } attributes?.transform = CGAffineTransform(translationX: translate, y: .zero)тАЛ return attributes } var additionalWidth: CGFloat { (dims.defaultSize.height * dims.aspectRatio - dims.defaultSize.width) * state.expanding } var size: CGSize { CGSize(width: dims.defaultSize.width + additionalWidth, height: dims.defaultSize.height) } var center: CGPoint { CGPoint(x: CGFloat(indexPath.row) * (dims.defaultSize.width + dims.inset) + dims.defaultSize.width / 2, y: dims.defaultSize.height / 2) } }
extension Cell {тАЛ func attributes(from layout: ThumbnailLayout, with sideCells: [Cell]) -> UICollectionViewLayoutAttributes? {тАЛ let attributes = layout.layoutAttributesForItem(at: indexPath)тАЛ attributes?.size = size attributes?.center = centerтАЛ let translate = sideCells.reduce(0) { (current, cell) -> CGFloat in if indexPath < cell.indexPath { return current - cell.additionalWidth / 2 } if indexPath > cell.indexPath { return current + cell.additionalWidth / 2 } return current } attributes?.transform = CGAffineTransform(translationX: translate, y: .zero)тАЛ return attributes } var additionalWidth: CGFloat { (dims.defaultSize.height * dims.aspectRatio - dims.defaultSize.width) * state.expanding } var size: CGSize { CGSize(width: dims.defaultSize.width + additionalWidth, height: dims.defaultSize.height) } var center: CGPoint { CGPoint(x: CGFloat(indexPath.row) * (dims.defaultSize.width + dims.inset) + dims.defaultSize.width / 2, y: dims.defaultSize.height / 2) } }
extension Cell {тАЛ func attributes(from layout: ThumbnailLayout, with sideCells: [Cell]) -> UICollectionViewLayoutAttributes? {тАЛ let attributes = layout.layoutAttributesForItem(at: indexPath)тАЛ attributes?.size = size attributes?.center = centerтАЛ let translate = sideCells.reduce(0) { (current, cell) -> CGFloat in if indexPath < cell.indexPath { return current - cell.additionalWidth / 2 } if indexPath > cell.indexPath { return current + cell.additionalWidth / 2 } return current } attributes?.transform = CGAffineTransform(translationX: translate, y: .zero)тАЛ return attributes } var additionalWidth: CGFloat { (dims.defaultSize.height * dims.aspectRatio - dims.defaultSize.width) * state.expanding } var size: CGSize { CGSize(width: dims.defaultSize.width + additionalWidth, height: dims.defaultSize.height) } var center: CGPoint { CGPoint(x: CGFloat(indexPath.row) * (dims.defaultSize.width + dims.inset) + dims.defaultSize.width / 2, y: dims.defaultSize.height / 2) } }
extension Cell {тАЛ func attributes(from layout: ThumbnailLayout, with sideCells: [Cell]) -> UICollectionViewLayoutAttributes? {тАЛ let attributes = layout.layoutAttributesForItem(at: indexPath)тАЛ attributes?.size = size attributes?.center = centerтАЛ let translate = sideCells.reduce(0) { (current, cell) -> CGFloat in if indexPath < cell.indexPath { return current - cell.additionalWidth / 2 } if indexPath > cell.indexPath { return current + cell.additionalWidth / 2 } return current } attributes?.transform = CGAffineTransform(translationX: translate, y: .zero)тАЛ return attributes } var additionalWidth: CGFloat { (dims.defaultSize.height * dims.aspectRatio - dims.defaultSize.width) * state.expanding } var size: CGSize { CGSize(width: dims.defaultSize.width + additionalWidth, height: dims.defaultSize.height) } var center: CGPoint { CGPoint(x: CGFloat(indexPath.row) * (dims.defaultSize.width + dims.inset) + dims.defaultSize.width / 2, y: dims.defaultSize.height / 2) } }
extension Cell {тАЛ func attributes(from layout: ThumbnailLayout, with sideCells: [Cell]) -> UICollectionViewLayoutAttributes? {тАЛ let attributes = layout.layoutAttributesForItem(at: indexPath)тАЛ attributes?.size = size attributes?.center = centerтАЛ let translate = sideCells.reduce(0) { (current, cell) -> CGFloat in if indexPath < cell.indexPath { return current - cell.additionalWidth / 2 } if indexPath > cell.indexPath { return current + cell.additionalWidth / 2 } return current } attributes?.transform = CGAffineTransform(translationX: translate, y: .zero)тАЛ return attributes } var additionalWidth: CGFloat { (dims.defaultSize.height * dims.aspectRatio - dims.defaultSize.width) * state.expanding } var size: CGSize { CGSize(width: dims.defaultSize.width + additionalWidth, height: dims.defaultSize.height) } var center: CGPoint { CGPoint(x: CGFloat(indexPath.row) * (dims.defaultSize.width + dims.inset) + dims.defaultSize.width / 2, y: dims.defaultSize.height / 2) } }
state.expanding
рдХреЛ рд▓рдЧрднрдЧ
state.expanding
рдХреЗ рд╕рдорд╛рди рдорд╛рдирд╛ рдЬрд╛рддрд╛ рд╣реИред
func cell(for index: IndexPath, offsetX: CGFloat) -> Cell {тАЛ let cell = Cell( indexPath: index, dims: Cell.Dimensions( defaultSize: itemSize, aspectRatio: dataSource(index.row), inset: config.distanceBetween, insetAsExpanded: config.distanceBetweenFocused), state: .default)тАЛ guard let attribute = cell.attributes(from: self, with: []) else { return cell }тАЛ let cellOffset = attribute.center.x - itemSize.width / 2 let widthWithOffset = itemSize.width + config.distanceBetween if abs(cellOffset - offsetX) < widthWithOffset { let expanding = 1 - abs(cellOffset - offsetX) / widthWithOffset return cell.updated(by: .expand(expanding)) } return cell }тАЛ override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return (0 ..< itemsCount) .map { IndexPath(row: $0, section: 0) } .map { cell(for: $0, offsetX: offsetWithoutInsets.x) } .compactMap { $0.attributes(from: self, with: cells) } }
func cell(for index: IndexPath, offsetX: CGFloat) -> Cell {тАЛ let cell = Cell( indexPath: index, dims: Cell.Dimensions( defaultSize: itemSize, aspectRatio: dataSource(index.row), inset: config.distanceBetween, insetAsExpanded: config.distanceBetweenFocused), state: .default)тАЛ guard let attribute = cell.attributes(from: self, with: []) else { return cell }тАЛ let cellOffset = attribute.center.x - itemSize.width / 2 let widthWithOffset = itemSize.width + config.distanceBetween if abs(cellOffset - offsetX) < widthWithOffset { let expanding = 1 - abs(cellOffset - offsetX) / widthWithOffset return cell.updated(by: .expand(expanding)) } return cell }тАЛ override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return (0 ..< itemsCount) .map { IndexPath(row: $0, section: 0) } .map { cell(for: $0, offsetX: offsetWithoutInsets.x) } .compactMap { $0.attributes(from: self, with: cells) } }
func cell(for index: IndexPath, offsetX: CGFloat) -> Cell {тАЛ let cell = Cell( indexPath: index, dims: Cell.Dimensions( defaultSize: itemSize, aspectRatio: dataSource(index.row), inset: config.distanceBetween, insetAsExpanded: config.distanceBetweenFocused), state: .default)тАЛ guard let attribute = cell.attributes(from: self, with: []) else { return cell }тАЛ let cellOffset = attribute.center.x - itemSize.width / 2 let widthWithOffset = itemSize.width + config.distanceBetween if abs(cellOffset - offsetX) < widthWithOffset { let expanding = 1 - abs(cellOffset - offsetX) / widthWithOffset return cell.updated(by: .expand(expanding)) } return cell }тАЛ override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return (0 ..< itemsCount) .map { IndexPath(row: $0, section: 0) } .map { cell(for: $0, offsetX: offsetWithoutInsets.x) } .compactMap { $0.attributes(from: self, with: cells) } }
func cell(for index: IndexPath, offsetX: CGFloat) -> Cell {тАЛ let cell = Cell( indexPath: index, dims: Cell.Dimensions( defaultSize: itemSize, aspectRatio: dataSource(index.row), inset: config.distanceBetween, insetAsExpanded: config.distanceBetweenFocused), state: .default)тАЛ guard let attribute = cell.attributes(from: self, with: []) else { return cell }тАЛ let cellOffset = attribute.center.x - itemSize.width / 2 let widthWithOffset = itemSize.width + config.distanceBetween if abs(cellOffset - offsetX) < widthWithOffset { let expanding = 1 - abs(cellOffset - offsetX) / widthWithOffset return cell.updated(by: .expand(expanding)) } return cell }тАЛ override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return (0 ..< itemsCount) .map { IndexPath(row: $0, section: 0) } .map { cell(for: $0, offsetX: offsetWithoutInsets.x) } .compactMap { $0.attributes(from: self, with: cells) } }
func cell(for index: IndexPath, offsetX: CGFloat) -> Cell {тАЛ let cell = Cell( indexPath: index, dims: Cell.Dimensions( defaultSize: itemSize, aspectRatio: dataSource(index.row), inset: config.distanceBetween, insetAsExpanded: config.distanceBetweenFocused), state: .default)тАЛ guard let attribute = cell.attributes(from: self, with: []) else { return cell }тАЛ let cellOffset = attribute.center.x - itemSize.width / 2 let widthWithOffset = itemSize.width + config.distanceBetween if abs(cellOffset - offsetX) < widthWithOffset { let expanding = 1 - abs(cellOffset - offsetX) / widthWithOffset return cell.updated(by: .expand(expanding)) } return cell }тАЛ override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return (0 ..< itemsCount) .map { IndexPath(row: $0, section: 0) } .map { cell(for: $0, offsetX: offsetWithoutInsets.x) } .compactMap { $0.attributes(from: self, with: cells) } }
4. рдПрдХ рдЫреЛрдЯреЗ рд╕реЗрд▓ рдХреЗ рддрддреНрд╡реЛрдВ рдХреЛ рд░рдЦрдиреЗ рдХрд╛ рддрд░реНрдХ рди рдХреЗрд╡рд▓ рд╕рд╛рдордЧреНрд░реА рдкрд░ рдирд┐рд░реНрднрд░ рдХрд░рддрд╛ рд╣реИ, рдмрд▓реНрдХрд┐ рдЙрдкрдпреЛрдЧрдХрд░реНрддрд╛ рдХреА рдмрд╛рддрдЪреАрдд рдкрд░ рднреА рдирд┐рд░реНрднрд░ рдХрд░рддрд╛ рд╣реИ
рдЬрдм рдХреЛрдИ рдЙрдкрдпреЛрдЧрдХрд░реНрддрд╛ рдПрдХ рдЫреЛрдЯреЗ рд╕рдВрдЧреНрд░рд╣ рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ рд╕реНрдХреНрд░реЙрд▓ рдХрд░рддрд╛ рд╣реИ, рддреЛ рд╕рднреА рдХреЛрд╢рд┐рдХрд╛рдПрдВ рд╕рдорд╛рди рдЖрдХрд╛рд░ рдХреА рд╣реЛрддреА рд╣реИрдВред рдмрдбрд╝реЗ рд╕рдВрдЧреНрд░рд╣ рдХреЛ рд╕реНрдХреНрд░реЙрд▓ рдХрд░рддреЗ рд╕рдордп, рдРрд╕рд╛ рдирд╣реАрдВ рд╣реИред (
рджреЗрдЦреЗрдВ gifs 3 рдФрд░ 5 )ред рдЖрдЗрдП рдПрдХ рдПрдирд┐рдореЗрдЯрд░ рд▓рд┐рдЦреЗрдВ рдЬреЛ
ThumbnailLayout
рд▓реЗрдЖрдЙрдЯ рдХреЗ рдЧреБрдгреЛрдВ рдХреЛ рдЕрдкрдбреЗрдЯ рдХрд░реЗрдЧрд╛ред рдПрдирд┐рдореЗрдЯрд░ рдЕрдкрдиреЗ рдЖрдк рдореЗрдВ
DisplayLink
рд╕реНрдЯреЛрд░ рдХрд░реЗрдЧрд╛ рдФрд░ рд╡рд░реНрддрдорд╛рди рдкреНрд░рдЧрддрд┐ рддрдХ рдкрд╣реБрдВрдЪ рдкреНрд░рджрд╛рди рдХрд░рддреЗ рд╣реБрдП, рдкреНрд░рддрд┐ рд╕реЗрдХрдВрдб 60 рдмрд╛рд░ рдмреНрд▓реЙрдХ рдХреЛ рдХреЙрд▓ рдХрд░реЗрдЧрд╛ред рдПрдирд┐рдореЗрдЯрд░ рдХреЛ рд╡рд┐рднрд┐рдиреНрди
easing functions
рдХреЛ
easing functions
ред рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рдХреЛ рдкреЛрд╕реНрдЯ рдХреЗ рдЕрдВрдд рдореЗрдВ рд▓рд┐рдВрдХ рдкрд░ рдЧрд┐рдердм рдкрд░ рджреЗрдЦрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИред
рдЖрдЗрдП
ThumbnailLayout
рдореЗрдВ
expandingRate
рд╕рдВрдкрддреНрддрд┐ рджрд░реНрдЬ рдХрд░реЗрдВ, рдЬрд┐рд╕рдХреЗ рджреНрд╡рд╛рд░рд╛ рд╕рднреА
Cell
expanding
рдЧреБрдгрд╛ рдХрд┐рдпрд╛ рдЬрд╛рдПрдЧрд╛ред рдпрд╣ рдкрддрд╛ рдЪрд▓рддрд╛ рд╣реИ рдХрд┐
expandingRate
рдХрд╣рддрд╛ рд╣реИ рдХрд┐
aspectRatio
рд╡рд┐рд╢реЗрд╖ рдЪрд┐рддреНрд░
aspectRatio
рдХрд┐рддрдирд╛
aspectRatio
рдЙрд╕рдХреЗ рдЖрдХрд╛рд░ рдХреЛ рдкреНрд░рднрд╛рд╡рд┐рдд рдХрд░реЗрдЧрд╛ рдпрджрд┐ рд╡рд╣ рдХреЗрдиреНрджреНрд░рд┐рдд рд╣реЛ рдЬрд╛рддрд╛ рд╣реИред
expandingRate == 0
рд╕рд╛рде
expandingRate == 0
рд╕рднреА рдХреЛрд╢рд┐рдХрд╛рдПрдБ рд╕рдорд╛рди рдЖрдХрд╛рд░ рдХреА рд╣реЛрдВрдЧреАред рдПрдХ рдЫреЛрдЯреЗ рд╕рдВрдЧреНрд░рд╣ рдХреА рд╕реНрдХреНрд░реЙрд▓ рдХреА рд╢реБрд░реБрдЖрдд рдореЗрдВ, рд╣рдо рдПрдХ рдПрдирд┐рдореЗрдЯрд░ рдЪрд▓рд╛рдПрдВрдЧреЗ, рдЬреЛ
expandingRate
рдХреЛ 0 рдкрд░ рд╕реЗрдЯ рдХрд░рддрд╛ рд╣реИ, рдФрд░ рд╕реНрдХреНрд░реЙрд▓ рдХреЗ рдЕрдВрдд рдореЗрдВ, рдЗрд╕рдХреЗ рд╡рд┐рдкрд░реАрдд, 1. рд╡рд╛рд╕реНрддрд╡ рдореЗрдВ, рд▓реЗрдЖрдЙрдЯ рдХреЛ рдЕрдкрдбреЗрдЯ рдХрд░рддреЗ рд╕рдордп, рдХреЗрдВрджреНрд░реАрдп рд╕реЗрд▓ рдФрд░ рд╕рд╛рдЗрдб рд╕реЗрд▓ рдХрд╛ рдЖрдХрд╛рд░ рдмрджрд▓ рдЬрд╛рдПрдЧрд╛ред
contentOffset
рдФрд░ рдорд░реЛрдбрд╝рддреЗ рд╣реБрдП рдХреЛрдИ
contentOffset
рдирд╣реАрдВ!
class ScrollAnimation: NSObject {тАЛ enum `Type` { case begin case end }тАЛ let type: TypeтАЛ func run(completion: @escaping () -> Void) { let toValue: CGFloat = self.type == .begin ? 0 : 1 let currentExpanding = thumbnails.config.expandingRate let duration = TimeInterval(0.15 * abs(currentExpanding - toValue))тАЛ let animator = Animator(onProgress: { current, _ in let rate = currentExpanding + (toValue - currentExpanding) * current self.thumbnails.config.expandingRate = rate self.thumbnails.invalidateLayout() }, easing: .easeInOut)тАЛ animator.animate(duration: duration) { _ in completion() } } }
class ScrollAnimation: NSObject {тАЛ enum `Type` { case begin case end }тАЛ let type: TypeтАЛ func run(completion: @escaping () -> Void) { let toValue: CGFloat = self.type == .begin ? 0 : 1 let currentExpanding = thumbnails.config.expandingRate let duration = TimeInterval(0.15 * abs(currentExpanding - toValue))тАЛ let animator = Animator(onProgress: { current, _ in let rate = currentExpanding + (toValue - currentExpanding) * current self.thumbnails.config.expandingRate = rate self.thumbnails.invalidateLayout() }, easing: .easeInOut)тАЛ animator.animate(duration: duration) { _ in completion() } } }
class ScrollAnimation: NSObject {тАЛ enum `Type` { case begin case end }тАЛ let type: TypeтАЛ func run(completion: @escaping () -> Void) { let toValue: CGFloat = self.type == .begin ? 0 : 1 let currentExpanding = thumbnails.config.expandingRate let duration = TimeInterval(0.15 * abs(currentExpanding - toValue))тАЛ let animator = Animator(onProgress: { current, _ in let rate = currentExpanding + (toValue - currentExpanding) * current self.thumbnails.config.expandingRate = rate self.thumbnails.invalidateLayout() }, easing: .easeInOut)тАЛ animator.animate(duration: duration) { _ in completion() } } }
class ScrollAnimation: NSObject {тАЛ enum `Type` { case begin case end }тАЛ let type: TypeтАЛ func run(completion: @escaping () -> Void) { let toValue: CGFloat = self.type == .begin ? 0 : 1 let currentExpanding = thumbnails.config.expandingRate let duration = TimeInterval(0.15 * abs(currentExpanding - toValue))тАЛ let animator = Animator(onProgress: { current, _ in let rate = currentExpanding + (toValue - currentExpanding) * current self.thumbnails.config.expandingRate = rate self.thumbnails.invalidateLayout() }, easing: .easeInOut)тАЛ animator.animate(duration: duration) { _ in completion() } } }
class ScrollAnimation: NSObject {тАЛ enum `Type` { case begin case end }тАЛ let type: TypeтАЛ func run(completion: @escaping () -> Void) { let toValue: CGFloat = self.type == .begin ? 0 : 1 let currentExpanding = thumbnails.config.expandingRate let duration = TimeInterval(0.15 * abs(currentExpanding - toValue))тАЛ let animator = Animator(onProgress: { current, _ in let rate = currentExpanding + (toValue - currentExpanding) * current self.thumbnails.config.expandingRate = rate self.thumbnails.invalidateLayout() }, easing: .easeInOut)тАЛ animator.animate(duration: duration) { _ in completion() } } }
class ScrollAnimation: NSObject {тАЛ enum `Type` { case begin case end }тАЛ let type: TypeтАЛ func run(completion: @escaping () -> Void) { let toValue: CGFloat = self.type == .begin ? 0 : 1 let currentExpanding = thumbnails.config.expandingRate let duration = TimeInterval(0.15 * abs(currentExpanding - toValue))тАЛ let animator = Animator(onProgress: { current, _ in let rate = currentExpanding + (toValue - currentExpanding) * current self.thumbnails.config.expandingRate = rate self.thumbnails.invalidateLayout() }, easing: .easeInOut)тАЛ animator.animate(duration: duration) { _ in completion() } } }

func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { if scrollView == thumbnails.collectionView { handle(event: .beginScrolling)
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { if scrollView == thumbnails.collectionView { handle(event: .beginScrolling)
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { if scrollView == thumbnails.collectionView { handle(event: .beginScrolling)
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { if scrollView == thumbnails.collectionView { handle(event: .beginScrolling)
5. рдЪрд╛рд▓ рдФрд░ рд╣рдЯрд╛рдиреЗ рдХреЗ рд▓рд┐рдП рдХрд╕реНрдЯрдо рдПрдирд┐рдореЗрд╢рди
рдХрдИ рд▓реЗрдЦ рдмрддрд╛ рд░рд╣реЗ рд╣реИрдВ рдХрд┐ рдХреЛрд╢рд┐рдХрд╛рдУрдВ рдХреЛ рдЕрдкрдбреЗрдЯ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдХрд╕реНрдЯрдо рдПрдирд┐рдореЗрд╢рди рдХреИрд╕реЗ рдмрдирд╛рдПрдВ, рд▓реЗрдХрд┐рди рд╣рдорд╛рд░реЗ рдорд╛рдорд▓реЗ рдореЗрдВ рд╡реЗ рд╣рдорд╛рд░реА рдорджрдж рдирд╣реАрдВ рдХрд░реЗрдВрдЧреЗред рд▓реЗрдЦ рдФрд░ рдЯреНрдпреВрдЯреЛрд░рд┐рдпрд▓ рдПрдХ рдЕрджреНрдпрддрди рд╕реЗрд▓ рдХреА рд╡рд┐рд╢реЗрд╖рддрд╛рдУрдВ рдХреЛ рдУрд╡рд░рд░рд╛рдЗрдб рдХрд░рдиреЗ рдХрд╛ рд╡рд░реНрдгрди рдХрд░рддреЗ рд╣реИрдВред рд╣рдорд╛рд░реЗ рдорд╛рдорд▓реЗ рдореЗрдВ, рд╣рдЯрд╛рдП рдЧрдП рд╕реЗрд▓ рдХреЗ рд▓реЗрдЖрдЙрдЯ рдХреЛ рдмрджрд▓рдиреЗ рд╕реЗ рд╕рд╛рдЗрдб рдЗрдлреЗрдХреНрдЯ рд╣реЛрддреЗ рд╣реИрдВ - рдкрдбрд╝реЛрд╕реА рд╕реЗрд▓
expanding
, рдЬреЛ рдПрдиреАрдореЗрд╢рди рдХреЗ рджреМрд░рд╛рди рд╣рдЯрд╛рдП рдЧрдП рд╕реНрдерд╛рди рдХрд╛ рд╕реНрдерд╛рди рд▓реЗрддрд╛ рд╣реИ, рдмрджрд▓ рдЬрд╛рддрд╛ рд╣реИред
UICollectionViewFlowLayout
рдореЗрдВ рд╕рд╛рдордЧреНрд░реА рдЕрдкрдбреЗрдЯ рдХрд░рдирд╛ рдирд┐рдореНрдирд╛рдиреБрд╕рд╛рд░ рдХрд╛рдо рдХрд░рддрд╛ рд╣реИред рд╕реЗрд▓ рдХреЛ рд╣рдЯрд╛рдиреЗ / рдЬреЛрдбрд╝рдиреЗ рдХреЗ рдмрд╛рдж,
prepare(forCollectionViewUpdates:)
рд╡рд┐рдзрд┐ рд▓реЙрдиреНрдЪ рдХреА
UICollectionViewUpdateItem
, рдЬреЛ
UICollectionViewUpdateItem
рдХрд╛ рдПрдХ рд╕рд░рдгреА
UICollectionViewUpdateItem
, рдЬреЛ рд╣рдореЗрдВ рдмрддрд╛рддрд╛ рд╣реИ рдХрд┐ рдХреМрди рд╕реА рдХреЛрд╢рд┐рдХрд╛рдПрдБ рдХрд┐рд╕ рдЗрдВрдбреЗрдХреНрд╕ рдореЗрдВ рдЕрдкрдбреЗрдЯ / рдбрд┐рд▓реАрдЯ / рдЧрдпреА рд╣реИрдВред рдЕрдЧрд▓рд╛, рд▓реЗрдЖрдЙрдЯ рд╡рд┐рдзрд┐рдпреЛрдВ рдХреЗ рдПрдХ рд╕рдореВрд╣ рдХреЛ рдмреБрд▓рд╛рдПрдЧрд╛
finalLayoutAttributesForDisappearingItem(at:) initialLayoutAttributesForAppearingDecorationElement(ofKind:at:)
рдФрд░ рд╕рдЬрд╛рд╡рдЯ / рдкреВрд░рдХ рд╡рд┐рдЪрд╛рд░реЛрдВ рдХреЗ рд▓рд┐рдП рдЙрдирдХреЗ рджреЛрд╕реНрддред рдЬрдм рдЕрджреНрдпрддрди рдХрд┐рдП рдЧрдП рдбреЗрдЯрд╛ рдХреЗ рд▓рд┐рдП рд╡рд┐рд╢реЗрд╖рддрд╛рдПрдБ рдкреНрд░рд╛рдкреНрдд рдХреА рдЬрд╛рддреА рд╣реИрдВ, рддреЛ
finalizeCollectionViewUpdates
рдХрд╣рд╛ рдЬрд╛рддрд╛ рд╣реИред рдПрдкреНрдкрд▓:
рд╕рдВрдЧреНрд░рд╣ рджреГрд╢реНрдп рдЗрд╕ рдкрджреНрдзрддрд┐ рдХреЛ рд╕реНрдерд╛рди рдореЗрдВ рдХрд┐рд╕реА рднреА рдкрд░рд┐рд╡рд░реНрддрди рдХреЛ рдЪреЗрддрди рдХрд░рдиреЗ рд╕реЗ рдкрд╣рд▓реЗ рдЕрдВрддрд┐рдо рдЪрд░рдг рдХреЗ рд░реВрдк рдореЗрдВ рдХрд╣рддрд╛ рд╣реИред рдЗрд╕ рдкрджреНрдзрддрд┐ рдХреЛ рдПрдиреАрдореЗрд╢рди рдмреНрд▓реЙрдХ рдХреЗ рднреАрддрд░ рдХрд╣рд╛ рдЬрд╛рддрд╛ рд╣реИ рдЬрд┐рд╕рдХрд╛ рдЙрдкрдпреЛрдЧ рд╕рднреА рдкреНрд░рд╡рд┐рд╖реНрдЯрд┐, рд╡рд┐рд▓реЛрдкрди, рдФрд░ рдПрдирд┐рдореЗрд╢рди рдХреЛ рд╕реНрдерд╛рдирд╛рдВрддрд░рд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИ рддрд╛рдХрд┐ рдЖрдк рдЖрд╡рд╢реНрдпрдХрддрд╛рдиреБрд╕рд╛рд░ рдЗрд╕ рдкрджреНрдзрддрд┐ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рдЕрддрд┐рд░рд┐рдХреНрдд рдПрдирд┐рдореЗрд╢рди рдмрдирд╛ рд╕рдХреЗрдВред рдЕрдиреНрдпрдерд╛, рдЖрдк рдЕрдкрдиреЗ рд▓реЗрдЖрдЙрдЯ рдСрдмреНрдЬреЗрдХреНрдЯ рдХреА рд░рд╛рдЬреНрдп рдЬрд╛рдирдХрд╛рд░реА рдХреЛ рдкреНрд░рдмрдВрдзрд┐рдд рдХрд░рдиреЗ рд╕реЗ рдЬреБрдбрд╝реЗ рдХрд┐рд╕реА рднреА рдЕрдВрддрд┐рдо рдорд┐рдирдЯ рдХреЗ рдХрд╛рд░реНрдп рдХреЛ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдЗрд╕рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВред
рдкрд░реЗрд╢рд╛рдиреА рдпрд╣ рд╣реИ рдХрд┐ рд╣рдо рдХреЗрд╡рд▓
рдЕрдкрдбреЗрдЯ рдХрд┐рдП рдЧрдП рд╕реЗрд▓ рдХреЗ рд▓рд┐рдП рд╡рд┐рд╢реЗрд╖рддрд╛рдУрдВ рдХреЛ рд╡рд┐рд╢реЗрд╖рдЬреНрдЮ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ, рдФрд░ рд╣рдореЗрдВ рдЙрдиреНрд╣реЗрдВ рд╕рднреА рдХреЛрд╢рд┐рдХрд╛рдУрдВ рдХреЗ рд▓рд┐рдП, рдФрд░ рдЕрд▓рдЧ-рдЕрд▓рдЧ рддрд░реАрдХреЛрдВ рд╕реЗ рдмрджрд▓рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИред рдирдП рдХреЗрдВрджреНрд░ рд╕реЗрд▓ рдХреЛ
aspectRatio
рдмрджрд▓рдирд╛ рдЪрд╛рд╣рд┐рдП, рдФрд░ рдкрдХреНрд╖ рдХреЛ
transform
рдЪрд╛рд╣рд┐рдПред

рдпрд╣ рдЬрд╛рдВрдЪрдиреЗ рдХреЗ рдмрд╛рдж рдХрд┐ рд╡рд┐рд▓реЛрдкрди / рд╕рдореНрдорд┐рд▓рди рдХреЗ рджреМрд░рд╛рди рд╕рдВрдЧреНрд░рд╣ рдХреЛрд╢рд┐рдХрд╛рдУрдВ рдХрд╛ рдбрд┐рдлрд╝реЙрд▓реНрдЯ рдПрдиреАрдореЗрд╢рди рдХреИрд╕реЗ рдХрд╛рдо рдХрд░рддрд╛ рд╣реИ, рдпрд╣ рдЬреНрдЮрд╛рдд рд╣реЛ рдЧрдпрд╛ рдХрд┐
finalizeCollectionViewUpdates
рдореЗрдВ рдкрд░рдд рдкрд░рддреЛрдВ рдореЗрдВ
CABasicAnimation
рд╢рд╛рдорд┐рд▓ рд╣реИ, рдЬрд┐рд╕реЗ рдпрджрд┐ рдЖрдк рд╢реЗрд╖ рдХреЛрд╢рд┐рдХрд╛рдУрдВ рдХреЗ рд▓рд┐рдП рдПрдиреАрдореЗрд╢рди рдХреЛ рдЕрдиреБрдХреВрд▓рд┐рдд рдХрд░рдирд╛ рдЪрд╛рд╣рддреЗ рд╣реИрдВ рддреЛ рд╡рд╣рд╛рдВ рдмрджрд▓рд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИред рдЬрдм рд▓реЙрдЧ рджрд┐рдЦрд╛рдП рдЬрд╛рддреЗ рд╣реИрдВ рдХрд┐ рдЪреАрдЬреЗрдВ рдЦрд░рд╛рдм рд╣реЛ рдЬрд╛рддреА рд╣реИрдВ, рддреЛ
performBatchUpdates
рдФрд░
prepare(forCollectionViewUpdates:)
,
prepareAttributes(attributes:)
prepare(forCollectionViewUpdates:)
рдХрд╣рд╛ рдЬрд╛рддрд╛ рд╣реИ, рдФрд░ рдкрд╣рд▓реЗ рд╕реЗ рд╣реА рдЧрд▓рдд рд╕рдВрдЦреНрдпрд╛ рдореЗрдВ рд╕реЗрд▓ рд╣реЛ рд╕рдХрддреЗ рд╣реИрдВ, рд╣рд╛рд▓рд╛рдВрдХрд┐ рдЕрднреА рддрдХ
collectionViewUpdates
prepareAttributes(attributes:)
рд╢реБрд░реВ рдирд╣реАрдВ рд╣реБрдП рд╣реИрдВ, рдЗрд╕реЗ рдмрдирд╛рдП рд░рдЦрдирд╛ рдФрд░ рд╕рдордЭрдирд╛ рдмрд╣реБрдд рдореБрд╢реНрдХрд┐рд▓ рд╣реИред рдЗрд╕ рдмрд╛рд░реЗ рдореЗрдВ рдХреНрдпрд╛ рдХрд┐рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИ? рдЖрдк рдЗрди рдЕрдВрддрд░реНрдирд┐рд╣рд┐рдд рдПрдирд┐рдореЗрд╢рди рдХреЛ рдЕрдХреНрд╖рдо рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ!
final override func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) { super.prepare(forCollectionViewUpdates: updateItems) CATransaction.begin() CATransaction.setDisableActions(true) }тАЛ final override func finalizeCollectionViewUpdates() { CATransaction.commit() }
final override func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) { super.prepare(forCollectionViewUpdates: updateItems) CATransaction.begin() CATransaction.setDisableActions(true) }тАЛ final override func finalizeCollectionViewUpdates() { CATransaction.commit() }
рдкрд╣рд▓реЗ рд╕реЗ рд▓рд┐рдЦреЗ рдЧрдП рдПрдирд┐рдореЗрдЯрд░реЛрдВ рдХреЗ рд╕рд╛рде рд╕рд╢рд╕реНрддреНрд░, рд╣рдо рд╣рдЯрд╛рдиреЗ рдХреЗ рдЕрдиреБрд░реЛрдз рдкрд░ рд╕рднреА рдЖрд╡рд╢реНрдпрдХ рдПрдирд┐рдореЗрд╢рди рдХрд░реЗрдВрдЧреЗ, рдФрд░ рд╣рдо рдПрдиреАрдореЗрд╢рди рдХреЗ рдЕрдВрдд рдореЗрдВ
dataSource
рдЕрдкрдбреЗрдЯ рд▓реЙрдиреНрдЪ рдХрд░реЗрдВрдЧреЗред рдЗрд╕ рдкреНрд░рдХрд╛рд░, рд╣рдо рдЕрдкрдбреЗрдЯ рдХрд░рддреЗ рд╕рдордп рд╕рдВрдЧреНрд░рд╣ рдХреЗ рдПрдиреАрдореЗрд╢рди рдХреЛ рд╕рд░рд▓ рдХрд░реЗрдВрдЧреЗ, рдХреНрдпреЛрдВрдХрд┐ рд╣рдо рдЦреБрдж рдХреЛ рдирд┐рдпрдВрддреНрд░рд┐рдд рдХрд░рддреЗ рд╣реИрдВ рдХрд┐ рдХреЛрд╢рд┐рдХрд╛рдУрдВ рдХреА рд╕рдВрдЦреНрдпрд╛ рдХрдм рдмрджрд▓ рдЬрд╛рдПрдЧреАред
func delete( at indexPath: IndexPath, dataSourceUpdate: @escaping () -> Void, completion: (() -> Void)?) {тАЛ DeleteAnimation(thumbnails: thumbnails, preview: preview, index: indexPath).run { let previousCount = self.thumbnails.itemsCount if previousCount == indexPath.row + 1 { self.activeIndex = previousCount - 1 } dataSourceUpdate() self.thumbnails.collectionView?.deleteItems(at: [indexPath]) self.preview.collectionView?.deleteItems(at: [indexPath]) completion?() } }
func delete( at indexPath: IndexPath, dataSourceUpdate: @escaping () -> Void, completion: (() -> Void)?) {тАЛ DeleteAnimation(thumbnails: thumbnails, preview: preview, index: indexPath).run { let previousCount = self.thumbnails.itemsCount if previousCount == indexPath.row + 1 { self.activeIndex = previousCount - 1 } dataSourceUpdate() self.thumbnails.collectionView?.deleteItems(at: [indexPath]) self.preview.collectionView?.deleteItems(at: [indexPath]) completion?() } }
рдРрд╕реЗ рдПрдирд┐рдореЗрд╢рди рдХреИрд╕реЗ рдХрд╛рдо рдХрд░реЗрдВрдЧреЗ?
ThumbnailLayout
рд╣рдо рд╡реИрдХрд▓реНрдкрд┐рдХ рдмреНрд░реЛрд╢рд░ рд╕рдВрдЧреНрд░рд╣реАрдд рдХрд░рддреЗ рд╣реИрдВ рдЬреЛ рд╡рд┐рд╢рд┐рд╖реНрдЯ рдХреЛрд╢рд┐рдХрд╛рдУрдВ рдХреА рдЬреНрдпрд╛рдорд┐рддрд┐ рдХреЛ рдЕрдкрдбреЗрдЯ рдХрд░рддреЗ рд╣реИрдВред
class ThumbnailLayout {тАЛ typealias CellUpdate = (Cell) -> Cell var updates: [IndexPath: CellUpdate] = [:]
class ThumbnailLayout {тАЛ typealias CellUpdate = (Cell) -> Cell var updates: [IndexPath: CellUpdate] = [:]
class ThumbnailLayout {тАЛ typealias CellUpdate = (Cell) -> Cell var updates: [IndexPath: CellUpdate] = [:]
рдЗрд╕ рддрд░рд╣ рдХреЗ рдПрдХ рдЙрдкрдХрд░рдг рд╣реЛрдиреЗ рдкрд░, рдЖрдк рдХреЛрд╢рд┐рдХрд╛рдУрдВ рдХреА рдЬреНрдпрд╛рдорд┐рддрд┐ рдХреЗ рд╕рд╛рде рдХреБрдЫ рднреА рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ, рдПрдирд┐рдореЗрдЯрд░ рдХреЗ рдХрд╛рдо рдХреЗ рджреМрд░рд╛рди рдЕрдкрдбреЗрдЯреНрд╕ рдХреЛ рдлреЗрдВрдХ рд╕рдХрддреЗ рд╣реИрдВ рдФрд░ рдЙрдиреНрд╣реЗрдВ рддрд╛рд░реАрдл рдореЗрдВ рд╣рдЯрд╛ рд╕рдХрддреЗ рд╣реИрдВред рдЕрдкрдбреЗрдЯ рдХреЗ рд╕рдВрдпреЛрдЬрди рдХреА рд╕рдВрднрд╛рд╡рдирд╛ рднреА рд╣реИред
updates[index] = newUpdate(updates[index])
рдбрд┐рд▓реАрдЯ рдПрдиреАрдореЗрд╢рди рдХреЛрдб рдмрд▓реНрдХрд┐ рдмреЛрдЭрд┐рд▓ рд╣реИ, рдпрд╣ рд░рд┐рдкреЙрдЬрд┐рдЯрд░реА рдореЗрдВ
DeleteAnimation.swift рдлрд╛рдЗрд▓ рдореЗрдВ рд╕реНрдерд┐рдд рд╣реИред рдХреЛрд╢рд┐рдХрд╛рдУрдВ рдХреЗ рдмреАрдЪ рдлреЛрдХрд╕ рд╕реНрд╡рд┐рдЪрд┐рдВрдЧ рдХрд╛ рдПрдиреАрдореЗрд╢рди рдЙрд╕реА рддрд░рд╣ рд╕реЗ рд▓рд╛рдЧреВ рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИред

рдЕрднрд┐рд╡рд┐рдиреНрдпрд╛рд╕ рдмрджрд▓рддреЗ рд╕рдордп "рд╕рдХреНрд░рд┐рдп" рд╕реЗрд▓ рдХрд╛ рд╕реВрдЪрдХрд╛рдВрдХ рдЦреЛ рдирд╣реАрдВ рдЬрд╛рддрд╛ рд╣реИ
scrollViewDidScroll(_ scrollView:)
рдХреЛ рднрд▓реЗ рд╣реА рдЖрдк
contentOffset
рдореЗрдВ рдХреБрдЫ рдореВрд▓реНрдп рдореЗрдВ рдкреЙрдк рдХрд░рдиреЗ рдХреЗ рд╕рд╛рде-рд╕рд╛рде рдУрд░рд┐рдПрдВрдЯреЗрд╢рди рдмрджрд▓рддреЗ рд╕рдордп рднреА рдХрд╣рддреЗ рд╣реИрдВред рдЬрдм рджреЛ рд╕рдВрдЧреНрд░рд╣реЛрдВ рдХреА рд╕реНрдХреНрд░реЙрд▓ рдХреЛ рд╕рд┐рдВрдХреНрд░рдирд╛рдЗрдЬрд╝ рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИ, рддреЛ рд▓реЗрдЖрдЙрдЯ рдХреЗ рдЕрдкрдбреЗрдЯ рдХреЗ рджреМрд░рд╛рди рдХреБрдЫ рд╕рдорд╕реНрдпрд╛рдПрдВ рдЙрддреНрдкрдиреНрди рд╣реЛ рд╕рдХрддреА рд╣реИрдВред рдирд┐рдореНрдирд▓рд┐рдЦрд┐рдд рдЪрд╛рд▓ рд╕реЗ рдорджрдж рдорд┐рд▓рддреА рд╣реИ: рд▓реЗрдЖрдЙрдЯ рдЕрдкрдбреЗрдЯ рдкрд░, рдЖрдк
scrollView.delegate
рдХреЛ рд╕реЗрдЯ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВред
extension ScrollSynchronizer {тАЛ private func bind() { preview.collectionView?.delegate = self thumbnails.collectionView?.delegate = self }тАЛ private func unbind() { preview.collectionView?.delegate = nil thumbnails.collectionView?.delegate = nil } }
extension ScrollSynchronizer {тАЛ private func bind() { preview.collectionView?.delegate = self thumbnails.collectionView?.delegate = self }тАЛ private func unbind() { preview.collectionView?.delegate = nil thumbnails.collectionView?.delegate = nil } }
extension ScrollSynchronizer {тАЛ private func bind() { preview.collectionView?.delegate = self thumbnails.collectionView?.delegate = self }тАЛ private func unbind() { preview.collectionView?.delegate = nil thumbnails.collectionView?.delegate = nil } }
рдЕрднрд┐рд╡рд┐рдиреНрдпрд╛рд╕ рдкрд░рд┐рд╡рд░реНрддрди рдХреЗ рд╕рдордп рд╕реЗрд▓ рдЖрдХрд╛рд░ рдХреЛ рдЕрдкрдбреЗрдЯ рдХрд░рддреЗ рд╕рдордп, рдпрд╣ рдЗрд╕ рддрд░рд╣ рджрд┐рдЦрд╛рдИ рджреЗрдЧрд╛:
extension PhotosViewController {тАЛ override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator)тАЛ contentView.synchronizer.unbind() coordinator.animate(alongsideTransition: nil) { [weak self] _ in self?.contentView.synchronizer.bind() } } }
extension PhotosViewController {тАЛ override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator)тАЛ contentView.synchronizer.unbind() coordinator.animate(alongsideTransition: nil) { [weak self] _ in self?.contentView.synchronizer.bind() } } }
extension PhotosViewController {тАЛ override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator)тАЛ contentView.synchronizer.unbind() coordinator.animate(alongsideTransition: nil) { [weak self] _ in self?.contentView.synchronizer.bind() } } }
рдЕрднрд┐рд╡рд┐рдиреНрдпрд╛рд╕ рдмрджрд▓рддреЗ рд╕рдордп рд╡рд╛рдВрдЫрд┐рдд
contentOffset
рдЦреЛрдиреЗ рдХреЗ рд▓рд┐рдП рдирд╣реАрдВ, рдЖрдк
targetIndexPath
рдореЗрдВ
targetIndexPath
рдХреЛ рдЕрдкрдбреЗрдЯ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВред рдЬрдм рдЖрдк рдЕрднрд┐рд╡рд┐рдиреНрдпрд╛рд╕ рдмрджрд▓рддреЗ рд╣реИрдВ, рддреЛ рд▓реЗрдЖрдЙрдЯ рдирд┐рд╖реНрдХреНрд░рд┐рдп рд╣реЛ рдЬрд╛рдПрдЧрд╛ рдпрджрд┐ рдЖрдк рдУрд╡рд░рд░рд╛рдЗрдб рдХрд░рдирд╛
shouldInvalidateLayout(forBoundsChange:)
ред
shouldInvalidateLayout(forBoundsChange:)
ред
bounds
рдмрджрд▓рддреЗ рд╕рдордп
bounds
рд▓реЗрдЖрдЙрдЯ
targetContentOffset(forProposedContentOffset:)
рдХреЛ рд╕реНрдкрд╖реНрдЯ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП
targetContentOffset(forProposedContentOffset:)
, рдЗрд╕реЗ рд╕реНрдкрд╖реНрдЯ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рдЖрдкрдХреЛ
targetContentOffset(forProposedContentOffset:)
рдХреЛ рдлрд┐рд░ рд╕реЗ рдкрд░рд┐рднрд╛рд╖рд┐рдд рдХрд░рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛
targetContentOffset(forProposedContentOffset:)
ред рдПрдкреНрдкрд▓:
рд▓реЗрдЖрдЙрдЯ рдЕрдкрдбреЗрдЯ рдХреЗ рджреМрд░рд╛рди, рдпрд╛ рд▓реЗрдЖрдЙрдЯ рдХреЗ рдмреАрдЪ рд╕рдВрдХреНрд░рдордг рд╣реЛрдиреЗ рдкрд░, рд╕рдВрдЧреНрд░рд╣ рджреГрд╢реНрдп рдЗрд╕ рдкрджреНрдзрддрд┐ рдХреЛ рдЖрдкрдХреЛ рдПрдиреАрдореЗрд╢рди рдХреЗ рдЕрдВрдд рдореЗрдВ рдЙрдкрдпреЛрдЧ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдкреНрд░рд╕реНрддрд╛рд╡рд┐рдд рд╕рд╛рдордЧреНрд░реА рдСрдлрд╕реЗрдЯ рдХреЛ рдмрджрд▓рдиреЗ рдХрд╛ рдЕрд╡рд╕рд░ рджреЗрддрд╛ рд╣реИред рдЖрдк рдЗрд╕ рдкрджреНрдзрддрд┐ рдХреЛ рдУрд╡рд░рд░рд╛рдЗрдб рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ рдпрджрд┐ рдПрдирд┐рдореЗрд╢рди рдпрд╛ рд╕рдВрдХреНрд░рдордг рдХреЗ рдХрд╛рд░рдг рдЖрдЗрдЯрдо рдХреЛ рдЗрд╕ рддрд░рд╣ рд╕реЗ рдкреЛрд╕реНрдЯ рдХрд┐рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИ рдЬреЛ рдЖрдкрдХреЗ рдбрд┐рдЬрд╝рд╛рдЗрди рдХреЗ рд▓рд┐рдП рдЗрд╖реНрдЯрддрдо рдирд╣реАрдВ рд╣реИред
рд╕рдВрдЧреНрд░рд╣ рджреГрд╢реНрдп prepare()
рдФрд░ рд╕рдВрдЧреНрд░рд╣ рджреГрд╢реНрдп рдХреЙрд▓ рдХреЗ рддрд░реАрдХреЛрдВ рдХреЗ рдмрд╛рдж рдЗрд╕ рдкрджреНрдзрддрд┐ рдХреЛ рдХреЙрд▓ рдХрд░рддрд╛ рд╣реИред
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint { let targetOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset) guard let layoutHandler = layoutHandler else { return targetOffset } let offset = CGFloat(layoutHandler.targetIndex) / CGFloat(itemsCount) return CGPoint( x: collectionViewContentSize.width * offset - farInset, y: targetOffset.y) }
рдкрдврд╝рдиреЗ рдХреЗ рд▓рд┐рдП рдзрдиреНрдпрд╡рд╛рдж!рд╕рднреА рдХреЛрдб github.com/YetAnotherRzmn/PhotosApp рдкрд░ рджреЗрдЦреЗ рдЬрд╛ рд╕рдХрддреЗ рд╣реИрдВ