{"id":463200,"date":"2025-06-13T10:34:34","date_gmt":"2025-06-13T10:34:34","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=463200"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=463200","title":{"rendered":"<span>\u0422\u0432\u043e\u0440\u0447\u0435\u0441\u043a\u0430\u044f \u043f\u0435\u0440\u0435\u0440\u0430\u0431\u043e\u0442\u043a\u0430 MVVM\u00a0\u0438 TCA \u043d\u0430 \u043f\u0440\u0438\u043c\u0435\u0440\u0435 iOS<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/f19\/4e0\/775\/f194e0775b51816fc7a69444c74d5ec7.jpg\" alt=\"\u041a\u0430\u043a\u0430\u044f \u043b\u0443\u0447\u0448\u0435???\" title=\"\u041a\u0430\u043a\u0430\u044f \u043b\u0443\u0447\u0448\u0435???\" width=\"889\" height=\"848\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/f19\/4e0\/775\/f194e0775b51816fc7a69444c74d5ec7.jpg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/f19\/4e0\/775\/f194e0775b51816fc7a69444c74d5ec7.jpg 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>\u041a\u0430\u043a\u0430\u044f \u043b\u0443\u0447\u0448\u0435???<\/figcaption><\/div>\n<\/figure>\n<p>\u0412\u0441\u0435\u043c \u043f\u0440\u0438\u0432\u0435\u0442, \u043c\u0435\u043d\u044f \u0437\u043e\u0432\u0443\u0442 \u0414\u043c\u0438\u0442\u0440\u0438\u0439 \u041b\u043e\u0440\u0435\u043d\u0446, \u044f iOS-\u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u0432 IT-\u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 GRI. \u041d\u0430\u0448 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u043a\u043b\u0438\u0435\u043d\u0442 \u2014 Sunlight, \u0434\u043b\u044f \u043d\u0435\u0433\u043e \u043c\u044b \u0440\u0430\u0437\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u043c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u044b\u0445 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439 \u043f\u043e \u043f\u043e\u043b\u043d\u043e\u043c\u0443 \u0446\u0438\u043a\u043b\u0443 \u0438 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c \u0441\u0430\u0439\u0442.<\/p>\n<p>\u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0443 \u043f\u0440\u043e \u043d\u0430\u0448\u0443 \u043d\u043e\u0432\u0443\u044e \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0443 \u0434\u043b\u044f iOS-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438 \u043f\u043e\u0434\u0435\u043b\u044e\u0441\u044c \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u043c\u0438 \u0441\u043e\u0432\u0435\u0442\u0430\u043c\u0438, \u043a\u0430\u043a \u0443\u043f\u0440\u043e\u0441\u0442\u0438\u0442\u044c \u0441\u0435\u0431\u0435 \u0436\u0438\u0437\u043d\u044c \u0438 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u043a\u043e\u0434 \u0431\u043e\u043b\u0435\u0435 \u043b\u0430\u043a\u043e\u043d\u0438\u0447\u043d\u044b\u043c \u0438 \u0447\u0438\u0442\u0430\u0435\u043c\u044b\u043c.<\/p>\n<p>\u0417\u0430 \u043e\u0441\u043d\u043e\u0432\u0443 \u043c\u044b \u0432\u0437\u044f\u043b\u0438 \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0443\u00a0MVVM (<code>Model<\/code>\u2014<code>View<\/code>\u2014<code>ViewModel<\/code>), \u0432\u00a0\u043a\u043e\u0442\u043e\u0440\u043e\u0439 <code>View<\/code> \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430\u00a0\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445, \u0432\u0441\u044f \u0431\u0438\u0437\u043d\u0435\u0441 \u043b\u043e\u0433\u0438\u043a\u0430 \u0441\u043e\u0441\u0440\u0435\u0434\u043e\u0442\u043e\u0447\u0435\u043d\u0430 \u0432\u043d\u0443\u0442\u0440\u0438 <code>ViewModel<\/code>. <code>ViewModel<\/code> \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043e\u0442 <code>View<\/code>, \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442 \u0441\u0432\u043e\u0438 \u0434\u0430\u043d\u043d\u044b\u0435, \u0438 <code>View<\/code> \u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043e\u043c data binding \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442 \u0441\u0432\u043e\u0451 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435, \u0447\u0442\u043e \u043e\u0447\u0435\u043d\u044c \u0443\u0434\u043e\u0431\u043d\u043e. <code>Model<\/code>\u00a0\u2014 \u043c\u043e\u0434\u0435\u043b\u044c \u0434\u043b\u044f\u00a0\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0438 \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0434\u0430\u043d\u043d\u044b\u0445. <\/p>\n<p>\u0422\u0430\u043a\u0436\u0435 \u043c\u044b \u043e\u0431\u0440\u0430\u0442\u0438\u043b\u0438 \u0441\u0432\u043e\u0439 \u0432\u0437\u043e\u0440 \u0432 \u0441\u0442\u043e\u0440\u043e\u043d\u0443 TCA, \u0443 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0435\u0441\u0442\u044c:<\/p>\n<ul>\n<li>\n<p><code>UI<\/code>\u00a0\u2014 \u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445;<\/p>\n<\/li>\n<li>\n<p><code>Action<\/code>\u00a0\u2014 \u043d\u0430\u0431\u043e\u0440 \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0445 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439;<\/p>\n<\/li>\n<li>\n<p><code>State<\/code>\u00a0\u2014 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445;<\/p>\n<\/li>\n<li>\n<p><code>Environment<\/code>\u00a0\u2014 \u043d\u0430\u0431\u043e\u0440 \u0432\u043d\u0435\u0448\u043d\u0438\u0445 \u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432;<\/p>\n<\/li>\n<li>\n<p><code>Reducer<\/code>\u00a0\u2014 \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c, \u043c\u0435\u043d\u044f\u044e\u0449\u0438\u0439 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0438 \u043f\u043e\u0440\u043e\u0436\u0434\u0430\u044e\u0449\u0438\u0439 \u044d\u0444\u0444\u0435\u043a\u0442\u044b;<\/p>\n<\/li>\n<li>\n<p><code>Effect<\/code>\u00a0\u2014 \u0437\u0430\u0434\u0430\u0447\u0430, \u043f\u043e\u00a0\u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0432 Reducer \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442\u0441\u044f <code>Action<\/code>.<\/p>\n<\/li>\n<\/ul>\n<p>\u041a\u0430\u043a\u0438\u0435 \u0431\u044b\u043b\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u0438 \u043f\u043e\u0447\u0435\u043c\u0443 \u0440\u0435\u0448\u0438\u043b\u0438 \u0434\u043e\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0442\u044c \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0443? \u041f\u0440\u043e\u0435\u043a\u0442 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f Sunlight \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0441\u0442\u0430\u0440\u044b\u0439, \u0434\u0430 \u0438 \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044f \u043d\u0430 SwiftUI \u043d\u0435 \u0442\u0430\u043a \u0434\u0430\u0432\u043d\u043e \u043e\u0431\u0440\u0435\u043b\u0430 \u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u043e\u0441\u0442\u044c, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0432\u0441\u044f \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044f \u0432 \u043f\u0440\u043e\u0435\u043a\u0442\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0447\u0435\u0440\u0435\u0437 UIKit. SwiftUI, \u043d\u0430\u0447\u0438\u043d\u0430\u044f \u0441 iOS 14, \u0431\u043e\u043b\u0435\u0435-\u043c\u0435\u043d\u0435\u0435 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0432\u0435\u0440\u0441\u0442\u0430\u0442\u044c \u0441\u043b\u043e\u0436\u043d\u044b\u0435 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u044b, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0431\u044b\u043b\u0430 \u0437\u0430\u0434\u0430\u0447\u0430 \u0434\u0435\u043b\u0430\u0442\u044c \u043c\u043e\u0434\u0443\u043b\u0438 \u043a\u0430\u043a \u043d\u0430 UIKit, \u0442\u0430\u043a \u0438 \u043d\u0430 SwiftUI. \u0421\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e, \u043d\u0443\u0436\u0435\u043d \u0431\u044b\u043b Builder \u043c\u043e\u0434\u0443\u043b\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u043a\u0430\u043a <code>UIVIew<\/code>, \u0442\u0430\u043a \u0438 <code>View<\/code>, \u0438 \u043e\u0442\u0434\u0430\u0451\u0442 <code>UIViewController<\/code>. \u041f\u0440\u0438 \u044d\u0442\u043e\u043c \u0431\u044b\u043b\u043e  \u0431\u043e\u043b\u044c\u0448\u043e\u0435 \u0436\u0435\u043b\u0430\u043d\u0438\u0435 \u0430\u0431\u0441\u0442\u0440\u0430\u0433\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043e\u0442 \u043c\u043d\u043e\u0433\u043e\u043f\u043e\u0442\u043e\u0447\u043d\u043e\u0441\u0442\u0438, \u0443\u043d\u0435\u0441\u0442\u0438 \u0435\u0451 \u0432 \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0443 \u043c\u043e\u0434\u0443\u043b\u044f \u0438 \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0443 \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u0451\u0440\u0441\u0442\u043a\u0443 \u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0438, \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0438 \u043e \u0447\u0451\u043c \u043d\u0435 \u0434\u0443\u043c\u0430\u044f. <\/p>\n<p>\u041a\u0430\u043a \u0438\u0442\u043e\u0433 \u043e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f MVVM \u0438 TCA \u0440\u043e\u0434\u0438\u043b\u0430\u0441\u044c \u043a\u043e\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f, \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0449\u0430\u044f \u0432 \u0441\u0435\u0431\u0435 \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u0438\u0437 MVVM: <code>Model<\/code>, <code>View<\/code>, <code>ViewModel<\/code>, \u0438 \u043f\u043e\u043c\u0438\u043c\u043e \u044d\u0442\u043e\u0433\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u043b\u0438\u0441\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0438\u0437 TCA: <code>State<\/code>, <code>Reducer<\/code> \u0438 <code>Action<\/code>. \u0420\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u0438\u0445 \u043f\u043e \u043f\u043e\u0440\u044f\u0434\u043a\u0443.<\/p>\n<h2>State<\/h2>\n<p><strong>State<\/strong> \u2014 \u044d\u0442\u043e \u043e\u0431\u0449\u0430\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u0441 \u0434\u0430\u043d\u043d\u044b\u043c\u0438, \u0445\u0440\u0430\u043d\u044f\u0449\u0430\u044f \u0432 \u0441\u0435\u0431\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 <code>View<\/code> \u0438 \u0435\u0433\u043e <code>Subview<\/code>. \u041e\u0431\u044b\u0447\u043d\u043e \u0432 MVVM \u0432\u0441\u0435 \u044d\u0442\u0438 \u0434\u0430\u043d\u043d\u044b\u0435 \u00ab\u0440\u043e\u0441\u0441\u044b\u043f\u044c\u044e\u00bb \u0445\u0440\u0430\u043d\u044f\u0442\u0441\u044f \u0432\u043e <code>ViewModel<\/code>, \u0430 \u0432 \u043d\u0430\u0448\u0435\u0439 \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0435 \u0432\u0441\u0435 UI-\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0432\u044b\u043d\u0435\u0441\u0435\u043d\u044b \u0432 <code>State<\/code>. \u041e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u0434\u043b\u044f \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f <code>ViewModel<\/code>, \u043b\u0435\u0436\u0430\u0442 \u0432\u043d\u0443\u0442\u0440\u0438 <code>ViewModel<\/code>. \u041d\u0438\u0436\u0435 \u2014 \u043f\u0440\u0438\u043c\u0435\u0440 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 <code>State<\/code> \u0441 <code>@Published<\/code> \u0438 \u0432\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u043c\u044b\u043c\u0438 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u043c\u0438.<\/p>\n<details class=\"spoiler\">\n<summary>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f State<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"swift\">import Combine import UIKit  final class ProductCardState: ViewStateProtocol { \u00a0\u00a0\u00a0\u00a0\/\/ MARK: \u2014 Properties  \u00a0\u00a0\u00a0\u00a0@Published var article: String \u00a0\u00a0\u00a0\u00a0@Published var loadingState: LoadingState \u00a0\u00a0\u00a0\u00a0@Published var position: Position \u00a0\u00a0\u00a0\u00a0@Published var currentSlidingStep: Int \u00a0\u00a0\u00a0\u00a0@Published var isImageSliderVertical: Bool \u00a0\u00a0\u00a0\u00a0@Published var productImages: [NetworkImage] \u00a0\u00a0\u00a0\u00a0@Published var actualPrice: String \u00a0\u00a0\u00a0\u00a0@Published var initialPrice: String \u00a0\u00a0\u00a0\u00a0@Published var basketLoadingState: LoadingState \u00a0\u00a0\u00a0\u00a0@Published var isPriceCellVisible: Bool \u00a0\u00a0\u00a0\u00a0@Published var isAvailableToBuy: Bool \u00a0\u00a0\u00a0\u00a0@Published var isAddedToBasket: Bool \u00a0\u00a0\u00a0\u00a0@Published var bottomSafeAreaInset: CGFloat \u00a0\u00a0\u00a0\u00a0@Published var priceDescription: String  \u00a0\u00a0\u00a0\u00a0var shouldShowPriceInButton: Bool { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if case .bottom = position { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return true \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0} \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return !isPriceCellVisible \u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0var navigationHeaderOpacity: Double { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0switch position { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0case .bottom: \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a00 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0case .middle: \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a00 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0case .top: \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0} \u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0var offset: Double { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0switch position { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0case .bottom: \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0UIScreen.main.bounds.height \u2014 PublicConstant.initialOffset \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0case .middle: \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0isImageSliderVertical ? UIScreen.main.bounds.height \/ 2.0 : UIScreen.main.bounds.width \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0case .top: \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0PublicConstant.navBarHeight \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0} \u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0var navigationHeader: NavigationHeader.ViewState { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0.init( \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0article: article, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0loadingState: loadingState, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0opacity: navigationHeaderOpacity \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0) \u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0var priceCell: PriceCell.ViewState { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0.init( \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0name: \"\u0421\u0435\u0440\u0435\u0431\u0440\u044f\u043d\u043d\u044b\u0435 \u0447\u0430\u0441\u044b Bastet. \u0428\u0432\u0435\u0439\u0446\u0430\u0440\u0441\u043a\u0438\u0439 \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c \u0438 \u0437\u043d\u0430\u043c\u0435\u043d\u0438\u0442\u044b\u0435 \u0411\u0435\u043b\u043e\u0440\u0443\u0441\u0441\u043a\u0438\u0435 \u0441\u0442\u0440\u0435\u043b\u043a\u0438\", \/\/ stub data \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0bages: [\"\u041d\u041e\u0412\u0418\u041d\u041a\u0410\", \"\u0425\u0418\u0422\", \"\u0418\u0422\u0410\u041b\u0418\u042f\"], \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0actualPrice: actualPrice, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0initialPrice: initialPrice, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0priceDescription: priceDescription, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0position: position \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0) \u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0var footerButtons: FooterButtons.ViewState { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0.init( \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0loadingState: loadingState, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0bottomInset: bottomSafeAreaInset, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0basketLoadingState: basketLoadingState, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0actualPrice: actualPrice, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0initialPrice: initialPrice, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0shouldShowPriceInButton: shouldShowPriceInButton, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0isAvailableToBuy: isAvailableToBuy, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0isAddedToBasket: isAddedToBasket \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0) \u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0var imageSliderAssembly: ImageSliderAssembly.ViewState { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0get { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0.init( \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0currentStep: currentSlidingStep, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0loadingState: loadingState, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0position: position, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0initialOffset: PublicConstant.initialOffset, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0isImageSliderVertical: isImageSliderVertical, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0productImages: productImages \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0) \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0set { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0currentSlidingStep = newValue.currentStep \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0} \u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0\/\/ MARK: \u2014 Lifecycle \u00a0\u00a0\u00a0\u00a0init(input: ProductCard.Input?) { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0article = input?.article ?? \"\" \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0loadingState = .loading \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0position = .bottom \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0currentSlidingStep = 0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0isImageSliderVertical = true \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0productImages = [] \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0actualPrice = \"\" \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0initialPrice = \"\" \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0basketLoadingState = .hide \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0isPriceCellVisible = false \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0isAvailableToBuy = true \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0isAddedToBasket = false \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0bottomSafeAreaInset = 0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0priceDescription = \"\" \u00a0\u00a0\u00a0\u00a0} }  extension ProductCard.ViewState { \u00a0\u00a0\u00a0\u00a0enum Position { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0case bottom \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0case middle \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0case top \u00a0\u00a0\u00a0\u00a0} }  extension ProductCard.ViewState { \u00a0\u00a0\u00a0\u00a0enum PublicConstant { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0static let initialOffset = 146.0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0static let navBarHeight = 104.0   } }<\/code><\/pre>\n<\/div>\n<\/details>\n<p><code>State<\/code> \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 <code>ViewStateProtocol<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0438\u043c\u0435\u0435\u0442 \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0442\u043e\u0440 \u0441 <code>Input<\/code>: \u0447\u0442\u043e\u0431\u044b \u0431\u044b\u043b\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0445\u043e\u0434\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u043c\u043e\u0434\u0443\u043b\u044c \u0438 \u043c\u0435\u0442\u043e\u0434 <code>update()<\/code> \u0434\u043b\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432 \u043f\u043e\u0442\u043e\u043a\u0435 <code>main<\/code>.<\/p>\n<details class=\"spoiler\">\n<summary>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f ViewStateProtocol<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"swift\">\/\/ MARK: \u2014 ViewState  @MainActor protocol ViewStateProtocol: ObservableObject, Sendable { \u00a0\u00a0\u00a0\u00a0associatedtype Input \u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0init(input: Input?) }  \u00a0\u00a0\u00a0\u00a0  extension ViewStateProtocol {  func update(_ handler: @Sendable @MainActor (Self) -&gt; Void) async { \u00a0\u00a0\u00a0\u00a0await MainActor.run { handler(self) } \u00a0\u00a0\u00a0\u00a0} }<\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u042d\u0442\u043e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e, \u0442\u0430\u043a \u043a\u0430\u043a \u0432\u0441\u0435 UI-\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b, \u043e\u0442\u0432\u0435\u0447\u0430\u044e\u0449\u0438\u0435 \u0437\u0430 \u0432\u043d\u0435\u0448\u043d\u0438\u0439 \u0432\u0438\u0434 <code>View<\/code> \u0438 \u0435\u0433\u043e <code>Subview<\/code>, \u0445\u0440\u0430\u043d\u044f\u0442\u0441\u044f \u0432\u043d\u0443\u0442\u0440\u0438 <code>State<\/code>. \u0421\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e, \u043f\u0440\u0438 \u0438\u0445 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 <code>View<\/code> \u0441\u0440\u0430\u0437\u0443 \u043f\u0435\u0440\u0435\u0440\u0438\u0441\u043e\u0432\u044b\u0432\u0430\u0435\u0442 \u0441\u0432\u043e\u0451 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435.\u00a0 \u0421\u0430\u043c \u043f\u043e \u0441\u0435\u0431\u0435 <code>State<\/code> \u2014 \u044d\u0442\u043e \u043a\u043b\u0430\u0441\u0441, \u0442\u0430\u043a \u0443\u0434\u043e\u0431\u043d\u0435\u0435 \u0435\u0433\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0432 \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u0445 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044f\u0445 (<code>View<\/code>, <code>ViewModel<\/code>), \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0442\u044c \u0432 <code>Subview<\/code> (\u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u0434\u0430\u043b\u0435\u0435), \u0438 \u0432\u0441\u0435\u0433\u0434\u0430 \u044d\u0442\u043e \u043e\u0434\u0438\u043d \u0438 \u0442\u043e\u0442 \u0436\u0435 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440.\u00a0<\/p>\n<h2>View<\/h2>\n<p>View \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 <code>ViewProtocol<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0432 \u0441\u0432\u043e\u044e \u043e\u0447\u0435\u0440\u0435\u0434\u044c \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u0430\u0433\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0443 \u0432 \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0442\u043e\u0440 <code>State<\/code> \u0438 <code>Reducer<\/code> (\u043e \u043d\u0451\u043c \u0447\u0443\u0442\u044c \u043f\u043e\u0437\u0436\u0435).<\/p>\n<pre><code class=\"swift\">\/\/ MARK: \u2014 View protocol ViewProtocol {     associatedtype ViewState: ViewStateProtocol     associatedtype ViewModel: ViewModelProtocol          @MainActor     init(state: ViewState, reducer: Reducer&lt;ViewModel&gt;) }<\/code><\/pre>\n<details class=\"spoiler\">\n<summary>\u041f\u0440\u0438\u043c\u0435\u0440 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 View<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"swift\">import SwiftUI struct ProductCardView: View, ViewProtocol {        @ObservedObject var state: ProductCard.ViewState     let reducer: Reducer&lt;ProductCard.ViewModel&gt;        private var isDragGestureEnabled: Bool {         if case .bottom = state.position {             return true         }         return false     }        init(state: ProductCard.ViewState, reducer: Reducer&lt;ProductCard.ViewModel&gt;) {         self.state = state         self.reducer = reducer     }        var body: some View {         ProductCardViewLayout(             header: { header },             sideButtons: { sideButtons },             slider: { slider },             content: { content },             footer: { footer(geometry: $0) }         )         .onAppear { reducer(.viewDidLoad) }         .animation(.easeInOut(duration: 1.0), value: state.position)         .animation(.default, value: state.isPriceCellVisible)         .animation(.default, value: state.isAddedToBasket)         .animation(.default, value: state.basketLoadingState)         .animation(.default, value: state.loadingState)     }        private var header: some View {         NavigationHeader(             state: state.navigationHeader,             onAction: { reducer(.onNavigationHeaderAction($0)) }         )     }        private var sideButtons: some View {         SideButtons(             position: state.position,             loadingState: state.loadingState,             onAction: { reducer(.onSideButtonsAction($0)) }         )         .opacity(1 \u2014 state.navigationHeaderOpacity)     }        private var slider: some View {         ImageSliderAssembly(             state: $state.imageSliderAssembly,             onAction: { reducer(.onImageSliderAction($0)) }         )     }        private var content: some View {         ProductCardList(state: state, reducer: reducer)             .offset(y: state.offset)     }        private func footer(geometry: GeometryProxy) -&gt; some View {         FooterButtons(             state: state.footerButtons,             onAction: { reducer(.onFooterButtonsAction($0)) }         )         .onAppear {             reducer(.setBottomSafeAreaInset(geometry.safeAreaInsets.bottom))         }         .animation(.easeInOut(duration: 1.0), value: state.position)         .animation(.default, value: state.isPriceCellVisible)         .animation(.default, value: state.isAddedToBasket)         .animation(.default, value: state.basketLoadingState)         .animation(.default, value: state.loadingState)     } }<\/code><\/pre>\n<\/div>\n<\/details>\n<p><code>View<\/code> \u043c\u043e\u0436\u0435\u0442 \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e \u0447\u0438\u0442\u0430\u0442\u044c \u0432\u0441\u0435 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430 <code>State<\/code> \u0438 \u0434\u0430\u0436\u0435 \u0438\u0437\u043c\u0435\u043d\u044f\u0442\u044c \u0438\u0445 \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 <code>Binding<\/code>. \u042d\u0442\u043e \u0441\u0434\u0435\u043b\u0430\u043d\u043e \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u043d\u043e, \u0442\u0430\u043a \u043a\u0430\u043a \u043e\u0442 <code>Binding<\/code> \u043d\u0435 \u0445\u043e\u0442\u0435\u043b\u043e\u0441\u044c \u043e\u0442\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c\u0441\u044f, \u043d\u043e \u0438 \u0433\u043e\u0440\u043e\u0434\u0438\u0442\u044c \u043a\u0430\u0436\u0434\u044b\u0439 \u0440\u0430\u0437 <code>get {} set {}<\/code> \u0432 \u043a\u043e\u0434\u0435 \u0442\u043e\u0436\u0435 \u043d\u0435 \u0431\u044b\u043b\u043e \u0436\u0435\u043b\u0430\u043d\u0438\u044f. \u0415\u0441\u043b\u0438 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u0430\u043a\u043e\u0439-\u043b\u0438\u0431\u043e \u0438\u0437 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 <code>State<\/code> \u0431\u0435\u0437 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f <code>Binding<\/code> (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043d\u0430\u0436\u0430\u0442\u0438\u0435 \u043a\u043d\u043e\u043f\u043a\u0438), \u0442\u043e \u0432\u0441\u0451 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442 \u0442\u0440\u0430\u0434\u0438\u0446\u0438\u043e\u043d\u043d\u043e \u0447\u0435\u0440\u0435\u0437 <code>ViewModel<\/code>. \u0418 \u0442\u0443\u0442 \u043c\u044b \u0432\u0438\u0434\u0438\u043c, \u0447\u0442\u043e <code>View<\/code> \u043d\u0435 \u0438\u043c\u0435\u0435\u0442 \u043d\u0438\u043a\u0430\u043a\u043e\u0439 <code>ViewModel<\/code>, \u0437\u0430\u0442\u043e \u0438\u043c\u0435\u0435\u0442 \u043d\u0435\u043a\u0438\u0439 <code>Reducer<\/code>. \u0427\u0442\u043e \u0436\u0435 \u044d\u0442\u043e \u0437\u0430 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044c \u0442\u0430\u043a\u0430\u044f \u0438 \u0434\u043b\u044f \u0447\u0435\u0433\u043e \u043d\u0443\u0436\u043d\u0430?<\/p>\n<h2>Reducer<\/h2>\n<p><code>Reducer<\/code> \u2014 \u044d\u0442\u043e \u0432\u0441\u043f\u043e\u043c\u043e\u0433\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u0430\u0441\u0441 \u0434\u043b\u044f \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441 <code>ViewModel<\/code>. \u0422\u0430\u043a \u043a\u0430\u043a <code>ViewModel<\/code> \u044d\u0442\u043e \u0430\u043a\u0442\u043e\u0440, \u0442\u043e \u0435\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u044b \u0438 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b \u0447\u0435\u0440\u0435\u0437 <code>await<\/code>. \u0427\u0442\u043e\u0431\u044b \u043a\u043e\u0434 \u0431\u044b\u043b \u0447\u0438\u0449\u0435 \u0438 \u043a\u0430\u0436\u0434\u044b\u0439 \u0440\u0430\u0437 \u043d\u0435 \u043f\u0438\u0441\u0430\u0442\u044c \u043a\u043e\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044e <code>Task { await viewModel.handle(...) }<\/code>, \u043f\u0440\u0438\u043c\u0435\u043d\u0451\u043d <code>Reducer<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u0432 \u0441\u0435\u0431\u044f \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 <code>Action<\/code> \u0438 \u0434\u0430\u043b\u044c\u0448\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442 \u0432\u0441\u044e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0443\u044e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0443 \u043f\u043e\u0434 \u043a\u0430\u043f\u043e\u0442\u043e\u043c.\u00a0<\/p>\n<pre><code class=\"swift\">\/\/ MARK: \u2014 Reducer final class Reducer&lt;ViewModel&gt;: Sendable where ViewModel: ViewModelProtocol {     private let viewModel: ViewModel          init(viewModel: ViewModel) {         self.viewModel = viewModel     }          nonisolated func callAsFunction(_ action: ViewModel.Action) {         Task { [weak self] in             await self?.viewModel.handle(action)         }     } }<\/code><\/pre>\n<p>\u041f\u0440\u0438\u043c\u0435\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f <code>Reducer<\/code>:<\/p>\n<pre><code class=\"swift\">private var header: some View {         NavigationHeader(             state: state.navigationHeader,             onAction: { reducer(.onNavigationHeaderAction($0)) }         )     }<\/code><\/pre>\n<p>\u0422\u043e \u0435\u0441\u0442\u044c \u043f\u0440\u043e\u0441\u0442\u043e \u043e\u0442\u0434\u0430\u0451\u043c \u0432 <code>Reducer<\/code> \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 \u043a\u0435\u0439\u0441 \u0438\u0437 enum <code>Action<\/code> (\u0438\u043d\u043e\u0433\u0434\u0430 \u043e\u043d \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0439 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440), \u0438 \u0432\u0441\u0451 \u2014 \u043f\u043e\u0434 \u043a\u0430\u043f\u043e\u0442\u043e\u043c \u0438\u0434\u0451\u0442 \u0430\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u0430\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0432\u043d\u0443\u0442\u0440\u0438 <code>ViewModel<\/code>.<\/p>\n<p>\u0414\u043b\u044f \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441 <code>ViewModel<\/code> \u043f\u0440\u0435\u0434\u0443\u0441\u043c\u043e\u0442\u0440\u0435\u043d enum <code>Action<\/code>.\u00a0<\/p>\n<h2>Action<\/h2>\n<p>Action \u2014 \u044d\u0442\u043e enum, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u0432\u043e\u0438\u043c\u0438 \u043a\u0435\u0439\u0441\u0430\u043c\u0438 \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u044b\u0435 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f <code>View<\/code> \u0441 <code>ViewModel<\/code>. <code>View<\/code> \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e \u043e\u0431\u0440\u0430\u0449\u0430\u0442\u044c\u0441\u044f \u043a \u043c\u0435\u0442\u043e\u0434\u0430\u043c <code>ViewModel<\/code>, \u043e\u043d\u0438 \u043f\u043e \u0431\u043e\u043b\u044c\u0448\u0435\u0439 \u0447\u0430\u0441\u0442\u0438 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0435 \u0438 \u043d\u0430\u0440\u0443\u0436\u0443 \u00ab\u0442\u043e\u0440\u0447\u0438\u0442\u00bb \u0442\u043e\u043b\u044c\u043a\u043e \u0444\u0443\u043d\u043a\u0446\u0438\u044f <code>handle(_ action: Action)<\/code> \u0434\u043b\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u043a\u0435\u0439\u0441\u0430 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0434\u0435\u043a\u043b\u0430\u0440\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0432\u043d\u0443\u0442\u0440\u0438 enum <code>ViewModel<\/code>. \u041f\u043e \u0441\u0443\u0442\u0438, \u043e\u043d \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0432 \u0441\u0435\u0431\u0435 \u0432\u0441\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u044b\u0435 \u043c\u0435\u0442\u043e\u0434\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 <code>View<\/code> \u043c\u043e\u0436\u0435\u0442 \u0432\u044b\u0437\u0432\u0430\u0442\u044c \u0443 <code>ViewModel<\/code>, \u0441\u0432\u043e\u0435\u0433\u043e \u0440\u043e\u0434\u0430 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0434\u043b\u044f \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441 <code>ViewModel<\/code>.<\/p>\n<p>\u041f\u0440\u0438\u043c\u0435\u0440 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 <code>Action<\/code>:<\/p>\n<pre><code class=\"swift\">  enum Action {         case viewDidLoad         case dismiss         case updatePosition(CGFloat)         case setPriceCellVisible(Bool)         case saveLastSlidingStep(Int)         case setBottomSafeAreaInset(CGFloat)         case onNavigationHeaderAction(NavigationHeader.Action)         case onSideButtonsAction(SideButtons.Action)         case onImageSliderAction(ImageSliderAssembly.Action)         case onFooterButtonsAction(FooterButtons.Action)         case onPriceCellAction(PriceCell.Action)     }<\/code><\/pre>\n<h2>ViewModel<\/h2>\n<p>\u0423 \u043d\u0435\u0451 \u0442\u0430\u043a\u0430\u044f \u0436\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c, \u043a\u0430\u043a \u0432 \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0435 MVVM: \u043e\u043d\u0430 \u0438\u043d\u043a\u0430\u043f\u0441\u0443\u043b\u0438\u0440\u0443\u0435\u0442 \u0432\u0441\u044e \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0443, \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0443\u0435\u0442 \u0441 \u0441\u0435\u0440\u0432\u0438\u0441\u0430\u043c\u0438 \u0438 \u0440\u043e\u0443\u0442\u0435\u0440\u043e\u043c. <code>ViewModel<\/code> \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 <code>ViewModelProtocol<\/code>, \u0432 \u0440\u0430\u043c\u043a\u0430\u0445 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u0432 \u0441\u0435\u0431\u044f <code>Input<\/code>, <code>Output<\/code> \u0438 <code>Router<\/code>.\u00a0<\/p>\n<pre><code class=\"swift\">\/\/ MARK: \u2014 ViewModel protocol ViewModelProtocol: Sendable {     associatedtype Input     associatedtype Output     associatedtype Action     associatedtype ViewState: ViewStateProtocol     associatedtype Router: RouterProtocol          @MainActor     init(state: ViewState, input: Input?, output: Output?, router: Router?)          func handle(_ action: Action) async }<\/code><\/pre>\n<p><code>Input<\/code> \u043d\u0443\u0436\u0435\u043d \u0434\u043b\u044f \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0438\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 <code>ViewModel<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0435 \u043e\u0442\u043d\u043e\u0441\u044f\u0442\u0441\u044f \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e \u043a UI, \u043d\u043e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b \u0434\u043b\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0438.\u00a0<\/p>\n<p><code>Output<\/code> \u2014 \u044d\u0442\u043e \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430, \u0438\u043b\u0438 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b, \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0449\u0438\u0439 \u0432 \u0441\u0435\u0431\u0435 \u043c\u0435\u0442\u043e\u0434\u044b\/\u0437\u0430\u043c\u044b\u043a\u0430\u043d\u0438\u044f \u0434\u043b\u044f \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441 \u0432\u043d\u0435\u0448\u043d\u0438\u043c\u0438 \u043c\u043e\u0434\u0443\u043b\u044f\u043c\u0438, \u0441\u0432\u043e\u0435\u0433\u043e \u0440\u043e\u0434\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f <code>Delegate<\/code>. \u0412 \u0440\u0430\u043c\u043a\u0430\u0445 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u044d\u0442\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u043d\u0435 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0430.\u00a0<\/p>\n<p><code>Router<\/code> \u0442\u0440\u0430\u0434\u0438\u0446\u0438\u043e\u043d\u043d\u043e \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044e \u043f\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044e.<\/p>\n<p>\u041a\u0430\u043a \u0438 \u0432 MVVM, <code>ViewModel<\/code> \u043d\u0438\u0447\u0435\u0433\u043e \u043d\u0435 \u0437\u043d\u0430\u0435\u0442 \u043f\u0440\u043e <code>View<\/code>, \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u043e \u0447\u0435\u0440\u0435\u0437 \u0440\u0435\u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0439 \u043f\u043e\u0434\u0445\u043e\u0434.<\/p>\n<details class=\"spoiler\">\n<summary>\u041f\u0440\u0438\u043c\u0435\u0440 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 ViewModel<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"swift\">actor ProductCardViewModel: ViewModelProtocol {     \/\/ MARK: - Nested Types     enum Action {         case viewDidLoad         case dismiss         case updatePosition(CGFloat)         case setPriceCellVisible(Bool)         case saveLastSlidingStep(Int)         case setBottomSafeAreaInset(CGFloat)         case onNavigationHeaderAction(NavigationHeader.Action)         case onSideButtonsAction(SideButtons.Action)         case onImageSliderAction(ImageSliderAssembly.Action)         case onFooterButtonsAction(FooterButtons.Action)         case onPriceCellAction(PriceCell.Action)     }        \/\/ MARK: - Private Properties     private let router: ProductCard.Router?     private let state: ProductCard.ViewState     private let input: ProductCard.Input?     private let output: ProductCard.Output?     private var isAnimating = false         \/\/ MARK: - Initializer     init(         state: ProductCard.ViewState,         input: ProductCard.Input?,         output: ProductCard.Output?,         router: ProductCard.Router?     ) {         self.state = state         self.input = input         self.output = output         self.router = router     }        \/\/ MARK: - Internal Methods     func handle(_ action: Action) async {         switch action {         case .viewDidLoad:             await viewDidLoad()         case .dismiss:             await dismiss()         case let .updatePosition(transition):             await updatePosition(for: transition)         case let .setPriceCellVisible(isPriceCellVisible):             await setPriceCellVisible(isPriceCellVisible)         case let .saveLastSlidingStep(step):             await saveSlidingStep(step)         case let .setBottomSafeAreaInset(inset):             await setBottomSafeAreaInset(inset)         case let .onNavigationHeaderAction(action):             await handleNavigationHeader(action: action)         case let .onSideButtonsAction(action):             await handleSideButtons(action: action)         case let .onImageSliderAction(action):             await handleImageSlider(action: action)         case let .onFooterButtonsAction(action):             await handleFooterButtons(action: action)         case let .onPriceCellAction(action):             await handlePriceCell(action: action)         }     } }  \/\/ MARK: - Private Methods extension ProductCard.ViewModel {     private func handleNavigationHeader(action: NavigationHeader.Action) async {         switch action {         case .onTapBackButton:             await dismiss()         case .onTapFavoriteButton:             print(action)         case .onTapShareButton:             print(action)         case .onTapSimilarButton:             print(action)         case .onTapSetsButton:             print(action)         }     }        private func handleSideButtons(action: SideButtons.Action) async {         switch action {         case .onTapShareButton:             print(action)         case .onTapSimilarButton:             print(action)         case .onTapSetsButton:             print(action)         }     }        private func handleFooterButtons(action: FooterButtons.Action) async {         switch action {         case .onTapMapButton:             await switchPriceStyle()         case .onTapBasketButton:             await addToBasket()         }     }        private func handlePriceCell(action: PriceCell.Action) async {         switch action {         case let .onSetPriceCellVisible(isVisible):             await setPriceCellVisible(isVisible)         }     }        private func handleImageSlider(action: ImageSliderAssembly.Action) async {         switch action {         case let .onTapSlider(index):             print(index)         case let .onTapReview(index):             print(index)         case let .onSaveSlidingStep(step):             await saveSlidingStep(step)         }     }        private func setInitialState() async {          await state.update { $0.makeStubData() }     }        private func setBottomSafeAreaInset(_ inset: CGFloat) async {         await state.update { $0.bottomSafeAreaInset = inset }     }        private func dismiss() async {         await router?.dismiss()     }        private func saveSlidingStep(_ step: Int) async {         try? await Task.sleep(seconds: 0.1)         await state.update {             $0.currentSlidingStep = step         }     }        private func setPosition() async {         if await !state.isImageSliderVertical {             await state.update { $0.position = .middle }         }     }        private func setPriceCellVisible(_ isVisible: Bool) async {         await state.update { state in             state.isPriceCellVisible = isVisible         }     }        private func addToBasket() async {         await state.update {             $0.basketLoadingState = .loading         }         try? await Task.sleep(seconds: 1.5)         await state.update { state in             state.isAddedToBasket.toggle()             state.basketLoadingState = .hide         }     }        private func viewDidLoad() async {         try? await Task.sleep(seconds: 2)         await setInitialState()         await state.update { $0.loadingState = .hide }         await setPosition()     }        private func updatePosition(for transition: CGFloat) async {         guard !isAnimating else { return }         isAnimating = true         let position = await handle(transition: transition)         await state.update { $0.position = position }         try? await Task.sleep(seconds: Constant.animationDuration)         isAnimating = false     }        private func handle(transition: CGFloat) async -&gt; ProductCard.ViewState.Position {         switch await (state.position, transition) {         case (.bottom, 0...):             .middle         case (.middle, ...0):             await state.isImageSliderVertical ? .bottom : .middle         case (.middle, 0...):             .top         case (.top, ...0):             .middle         default:             await state.position         }     }        private func switchPriceStyle() async {         await state.update { $0.isPriceCellVisible.toggle() }     } }  extension ProductCard.ViewModel {     private enum Constant {         static let animationDuration = 1.0    } }<\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u041a\u0430\u043a \u0432\u0438\u0434\u0438\u0442\u0435, \u0443 <code>ViewModel<\/code> \u0435\u0441\u0442\u044c \u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0439 \u043c\u0435\u0442\u043e\u0434 <code>handle(_ action: Action) async<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u0432\u0441\u0435 \u043e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u044f \u0438\u0437 <code>View<\/code> \u0447\u0435\u0440\u0435\u0437 <code>Reducer<\/code>. \u0412\u0441\u0435 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u043c\u0435\u0442\u043e\u0434\u044b \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0435.\u00a0<\/p>\n<pre><code class=\"swift\"> func handle(_ action: Action) async {         switch action {         case .viewDidLoad:             await viewDidLoad()         case .dismiss:             await dismiss()         case let .updatePosition(transition):             await updatePosition(for: transition)         case let .setPriceCellVisible(isPriceCellVisible):             await setPriceCellVisible(isPriceCellVisible)         case let .saveLastSlidingStep(step):             await saveSlidingStep(step)         case let .setBottomSafeAreaInset(inset):             await setBottomSafeAreaInset(inset)         case let .onNavigationHeaderAction(action):             await handleNavigationHeader(action: action)         case let .onSideButtonsAction(action):             await handleSideButtons(action: action)         case let .onImageSliderAction(action):             await handleImageSlider(action: action)         case let .onFooterButtonsAction(action):             await handleFooterButtons(action: action)         case let .onPriceCellAction(action):             await handlePriceCell(action: action)         }     }<\/code><\/pre>\n<h2>Module<\/h2>\n<p>Module \u2014 \u044d\u0442\u043e \u043e\u0431\u0449\u0438\u0439 \u0444\u0430\u0439\u043b, \u0433\u0434\u0435 \u0445\u0440\u0430\u043d\u044f\u0442\u0441\u044f \u0432\u0441\u0435 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u044b, \u043e\u0442\u0432\u0435\u0447\u0430\u044e\u0449\u0438\u0435 \u0437\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043c\u043e\u0434\u0443\u043b\u044f. \u0422\u043e \u0435\u0441\u0442\u044c \u043e\u043d \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442 \u0432\u0441\u044e \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0443 \u0438 \u0435\u0451 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438.<\/p>\n<details class=\"spoiler\">\n<summary>\u041f\u0440\u0438\u043c\u0435\u0440 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 Module<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"swift\">import SwiftUI import Combine  \/\/ MARK: \u2014 ViewState @MainActor protocol ViewStateProtocol: ObservableObject, Sendable {     associatedtype Input          init(input: Input?) }      extension ViewStateProtocol { func update(_ handler: @Sendable @MainActor (Self) -&gt; Void) async {     await MainActor.run { handler(self) }     } }  \/\/ MARK: \u2014 ViewModel protocol ViewModelProtocol: Sendable {     associatedtype Input     associatedtype Output     associatedtype Action     associatedtype ViewState: ViewStateProtocol     associatedtype Router: RouterProtocol          @MainActor     init(state: ViewState, input: Input?, output: Output?, router: Router?)          func handle(_ action: Action) async }  \/\/ MARK: \u2014 View protocol ViewProtocol {     associatedtype ViewState: ViewStateProtocol     associatedtype ViewModel: ViewModelProtocol          @MainActor     init(state: ViewState, reducer: Reducer&lt;ViewModel&gt;) }  \/\/ MARK: \u2014 Router @MainActor protocol RouterProtocol: Sendable {     var parentViewController: UIViewController? { get set }     init() }  \/\/ MARK: \u2014 Reducer final class Reducer&lt;ViewModel&gt;: Sendable where ViewModel: ViewModelProtocol {     private let viewModel: ViewModel          init(viewModel: ViewModel) {         self.viewModel = viewModel     }          nonisolated func callAsFunction(_ action: ViewModel.Action) {         Task { [weak self] in             await self?.viewModel.handle(action)         }     } }  \/\/ MARK: \u2014 Module protocol ModuleProtocol {     associatedtype Input     associatedtype Output     associatedtype ViewState: ViewStateProtocol where ViewState.Input == Input     associatedtype ViewScene: ViewProtocol where ViewScene.ViewState == ViewState, ViewScene.ViewModel == ViewModel     associatedtype ViewModel: ViewModelProtocol where ViewModel.Input == Input, ViewModel.Output == Output, ViewModel.ViewState == ViewState, ViewModel.Router == Router     associatedtype Router: RouterProtocol }  extension ModuleProtocol {     @MainActor     static func build(input: Input? = nil, output: Output? = nil) -&gt; UIViewController {         let state = ViewState(input: input)         var router = Router()         let viewModel = ViewModel(             state: state,             input: input,             output: output,             router: router         )         let reducer = Reducer(viewModel: viewModel)         let view = ViewScene(state: state, reducer: reducer)                if let vc = view as? UIViewController {             router.parentViewController = vc             return vc         } else if let view = view as? (any View) {             let viewController = UIHostingController(rootView: AnyView(view))             router.parentViewController = viewController             return viewController         } else {             fatalError(\"Unexpected view type\")         }     } }  extension ModuleProtocol where ViewScene: View {     @MainActor     static func preview(input: Input? = nil, output: Output? = nil) -&gt; some View {         let state = ViewState(input: input)         let router = Router()         let viewModel = ViewModel(             state: state,             input: input,             output: output,             router: router         )         let reducer = Reducer(viewModel: viewModel)         return ViewScene(state: state, reducer: reducer)     } }  final class Builder&lt;M&gt;: Sendable where M: ModuleProtocol {     @MainActor     static func build(input: M.ViewModel.Input? = nil, output: M.ViewModel.Output? = nil) -&gt; UIViewController {         let state = M.ViewState(input: input)         var router = M.Router()         let viewModel = M.ViewModel(             state: state,             input: input,             output: output,             router: router         )         let reducer = Reducer(viewModel: viewModel)         let view = M.ViewScene(state: state, reducer: reducer)                if let vc = view as? UIViewController {             router.parentViewController = vc             return vc         } else if let view = view as? (any View) {             let viewController = UIHostingController(rootView: AnyView(view))             router.parentViewController = viewController             return viewController         } else {             fatalError(\"Unexpected view type\")         }     } }  extension Builder where M.ViewScene: View {     @MainActor     static func preview(input: M.ViewModel.Input? = nil, output: M.ViewModel.Output? = nil) -&gt; some View {         let state = M.ViewState(input: input)         let router = M.Router()         let viewModel = M.ViewModel(             state: state,             input: input,             output: output,             router: router         )         let reducer = Reducer(viewModel: viewModel)         return M.ViewScene(state: state, reducer: reducer)     } }<\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0420\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0433\u0440\u0430\u043d\u0438\u0447\u043d\u044b\u0435 \u0441\u043b\u0443\u0447\u0430\u0438 \u0434\u043b\u044f \u043a\u0440\u0430\u0441\u043e\u0442\u044b \u043a\u043e\u0434\u0430 \u0438 \u0443\u043f\u0440\u043e\u0449\u0435\u043d\u0438\u044f \u0432\u043e\u0441\u043f\u0440\u0438\u044f\u0442\u0438\u044f.<\/p>\n<h2>\u041f\u0435\u0440\u0435\u0434\u0430\u0447\u0430 \u0432\u0445\u043e\u0434\u043d\u044b\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0432 Subview<\/h2>\n<p>\u0415\u0441\u043b\u0438 \u0443 \u043a\u0430\u043a\u043e\u0439-\u043b\u0438\u0431\u043e <code>Subview<\/code> \u0438\u043c\u0435\u0435\u0442\u0441\u044f \u0431\u043e\u043b\u0435\u0435 \u0434\u0432\u0443\u0445 \u0432\u0445\u043e\u0434\u043d\u044b\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432, \u0442\u043e \u0447\u0442\u043e\u0431\u044b \u043d\u0435 \u0443\u0442\u044f\u0436\u0435\u043b\u044f\u0442\u044c \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0442\u043e\u0440 \u0438 \u043d\u0435 \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0442\u044c \u0442\u0443\u0434\u0430 \u043f\u043e\u0434 \u0434\u0435\u0441\u044f\u0442\u043e\u043a \u0432\u0445\u043e\u0434\u043d\u044b\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432, \u043c\u044b \u043c\u043e\u0436\u0435\u043c \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443 <code>ViewState<\/code> \u0432\u043d\u0443\u0442\u0440\u0438 \u0441\u0430\u043c\u043e\u0433\u043e <code>Subview<\/code>, \u0438 \u0442\u0430\u043c \u0443\u0436\u0435 \u043e\u043f\u0438\u0441\u0430\u0442\u044c \u0432\u0441\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0434\u043b\u044f \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u044d\u0442\u043e\u0433\u043e <code>Subview<\/code>.<\/p>\n<p>\u041f\u0440\u0438\u043c\u0435\u0440 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0432 <code>ImageSliderAssembly<\/code>:<\/p>\n<pre><code class=\"swift\">struct ImageSliderAssembly: View {     \/\/ MARK: \u2014 Nested Types          struct ViewState {         var currentStep: Int         let loadingState: LoadingState         let position: ProductCard.ViewState.Position         let initialOffset: CGFloat         let isImageSliderVertical: Bool         let productImages: [NetworkImage]     }<\/code><\/pre>\n<p>\u041b\u043e\u0433\u0438\u0447\u043d\u0435\u0435 \u0438 \u043b\u0430\u043a\u043e\u043d\u0438\u0447\u043d\u0435\u0435 \u0431\u044b\u043b\u043e \u0431\u044b \u043d\u0430\u0437\u0432\u0430\u0442\u044c \u0435\u0433\u043e \u043f\u0440\u043e\u0441\u0442\u043e <code>State<\/code>: \u043d\u043e \u0442\u043e\u0433\u0434\u0430 \u0435\u0441\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u0437\u0430\u0441\u0435\u0447\u043a\u0438 \u0441 \u043d\u0435\u0439\u043c\u0438\u043d\u0433\u043e\u043c \u043c\u043e\u0434\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 <code>@State<\/code>, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043f\u0440\u0438\u0448\u043b\u0438 \u043a \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044e <code>ViewState<\/code>.<\/p>\n<p>\u0412\u043e\u0442 \u0442\u0430\u043a \u044d\u0442\u043e \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0432\u043d\u0443\u0442\u0440\u0438 <code>View<\/code>:<\/p>\n<pre><code class=\"swift\"> private var slider: some View {         ImageSliderAssembly(             state: $state.imageSliderAssembly,             onAction: { reducer(.onImageSliderAction($0)) }         )     }<\/code><\/pre>\n<p>\u0412\u0441\u0435\u0433\u043e \u043e\u0434\u043d\u0430 \u0441\u0442\u0440\u043e\u0447\u043a\u0430 \u0438 \u043d\u0438\u043a\u0430\u043a\u043e\u0439 \u00ab\u043f\u043e\u0440\u0442\u044f\u043d\u043a\u0438\u00bb \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432.\u00a0<\/p>\n<p>\u0422\u0430\u043a \u044d\u0442\u043e \u0441\u043e\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044f \u0432 \u0435\u0434\u0438\u043d\u0443\u044e \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0443\u044e <code>imageSliderAssembly<\/code> \u0432\u043d\u0443\u0442\u0440\u0438 <code>State<\/code>:<\/p>\n<pre><code class=\"swift\"> var imageSliderAssembly: ImageSliderAssembly.ViewState {         get {             .init(                 currentStep: currentSlidingStep,                 loadingState: loadingState,                 position: position,                 initialOffset: PublicConstant.initialOffset,                 isImageSliderVertical: isImageSliderVertical,                 productImages: productImages             )         }         set {             currentSlidingStep = newValue.currentStep         }     }<\/code><\/pre>\n<p>\u0417\u0434\u0435\u0441\u044c \u0436\u0435 \u0432\u0438\u0434\u0438\u043c \u0447\u0430\u0441\u0442\u043d\u044b\u0439 \u0441\u043b\u0443\u0447\u0430\u0439, \u043a\u043e\u0433\u0434\u0430 \u0432 \u0432\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u043c\u043e\u0439 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c <code>Binding<\/code>. \u0412\u0441\u0451 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u043f\u043e\u043d\u044f\u0442\u043d\u043e \u0438 \u043b\u0430\u043a\u043e\u043d\u0438\u0447\u043d\u043e.<\/p>\n<h2>\u041f\u0435\u0440\u0435\u0434\u0430\u0447\u0430 \u0437\u0430\u043c\u044b\u043a\u0430\u043d\u0438\u0439 \u0432 Subview \u0434\u043b\u044f \u043e\u0442\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u043d\u0430\u0436\u0430\u0442\u0438\u044f \u043a\u043d\u043e\u043f\u043e\u043a \u0438 \u043f\u0440\u043e\u0447\u0435\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u0438<\/h2>\n<p>\u041a\u0430\u043a \u0438 \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u0441 \u0432\u0445\u043e\u0434\u043d\u044b\u043c\u0438 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430\u043c\u0438, <code>Subview<\/code> \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0442\u044c \u043c\u043d\u043e\u0433\u043e \u0437\u0430\u043c\u044b\u043a\u0430\u043d\u0438\u0439 (action), \u0438 \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0442\u044c \u044d\u0442\u043e \u043e\u0431\u0438\u043b\u0438\u0435 \u0432 \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0442\u043e\u0440\u0435 \u043d\u0435\u0443\u0434\u043e\u0431\u043d\u043e. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0443 \u043d\u0430\u0441 \u043a\u0430\u0436\u0434\u0430\u044f <code>Subview<\/code> \u043c\u043e\u0436\u0435\u0442 \u0438\u043c\u0435\u0442\u044c \u0441\u0432\u043e\u0439 \u043b\u0438\u0447\u043d\u044b\u0439 <code>Action<\/code>.<\/p>\n<p>\u0420\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u043d\u0430 \u043f\u0440\u0438\u043c\u0435\u0440\u0435 \u0442\u043e\u0433\u043e \u0436\u0435 <code>ImageSliderAssembly<\/code>:<\/p>\n<pre><code class=\"swift\"> struct ImageSliderAssembly: View {         \u2026      enum Action {         case onTapSlider(Int)         case onTapReview(Int)         case onSaveSlidingStep(Int)     }         \/\/ MARK: \u2014 Properties     @Binding var state: ViewState     let onAction: (Action) -&gt; Void<\/code><\/pre>\n<p>\u0412\u043d\u0443\u0442\u0440\u0438 <code>ImageSliderAssembly<\/code> \u0435\u0441\u0442\u044c \u0442\u0440\u0438 \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u0445 <code>action<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u044b \u0432 \u043e\u0434\u0438\u043d \u043e\u0431\u0449\u0438\u0439 <code>Action<\/code>. \u0422\u0430\u043a\u0436\u0435 \u0435\u0441\u0442\u044c \u043e\u0434\u043d\u043e-\u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0435 \u0437\u0430\u043c\u044b\u043a\u0430\u043d\u0438\u0435 <code>onAction<\/code>, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u043f\u0440\u0438\u0445\u043e\u0434\u0438\u0442 \u0438\u0437 \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0442\u043e\u0440\u0430 (\u0437\u0434\u0435\u0441\u044c \u043d\u0435 \u043f\u0440\u043e\u043f\u0438\u0441\u0430\u043d\u043e, \u0442\u0430\u043a \u043a\u0430\u043a \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043f\u043e\u0434 \u043a\u0430\u043f\u043e\u0442\u043e\u043c \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0442\u043e\u0440) \u0438 \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u0432\u0441\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u044b\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0432\u043d\u0443\u0442\u0440\u0438 \u044d\u0442\u043e\u0433\u043e <code>Subview<\/code>.<\/p>\n<p>\u041a\u0430\u043a \u044d\u0442\u043e \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0432\u043d\u0443\u0442\u0440\u0438 <code>Subview<\/code>:<\/p>\n<pre><code class=\"swift\">private var assembly: some View {         ZStack(alignment: .bottom) {             VStack(spacing: 0) {                 ImageSlider(                     currentStep: $state.currentStep,                     media: state.productImages,                     onTapSlider: { onAction(.onTapSlider($0)) },                     onSaveSlidingStep: { onAction(.onSaveSlidingStep($0)) }                 )                 .frame(height: sliderFrameHeight)                 if isNeedBottomSpacer { Spacer() }             }             SliderPageControl(totalSteps: state.productImages.count, currentStep: state.currentStep)         }     }<\/code><\/pre>\n<p>\u041f\u0440\u043e\u0441\u0442\u043e \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0437\u0430\u043c\u044b\u043a\u0430\u043d\u0438\u0435 <code>onAction<\/code>, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u043f\u0435\u0440\u0435\u0434\u0430\u0451\u0442\u0441\u044f \u043e\u0434\u0438\u043d \u0438\u0437 \u043a\u0435\u0439\u0441\u043e\u0432 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0435\u0433\u043e enum <code>Action<\/code>.\u00a0<\/p>\n<p>\u041a\u0430\u043a \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u0435\u0442\u0441\u044f <code>ImageSliderAssembly<\/code> \u0432\u043d\u0443\u0442\u0440\u0438 <code>View<\/code>:<\/p>\n<pre><code class=\"swift\"> private var slider: some View {         ImageSliderAssembly(             state: $state.imageSliderAssembly,             onAction: { reducer(.onImageSliderAction($0)) }         )     }<\/code><\/pre>\n<p>\u041e\u0431\u0449\u0438\u0439 <code>Action<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0436\u0438\u0432\u0451\u0442 \u0432\u043d\u0443\u0442\u0440\u0438 <code>ViewModel<\/code>, \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 case <code>onImageSliderAction<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0432 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0439 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 <code>ImageSliderAssembly.Action<\/code> \u2014 enum \u0432\u043d\u0443\u0442\u0440\u0438 <code>Subview<\/code>. \u041f\u0440\u0438 \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0437\u0430\u043c\u044b\u043a\u0430\u043d\u0438\u044f <code>onAction<\/code> \u043f\u0440\u043e\u0441\u0442\u043e \u043e\u0442\u0434\u0430\u0451\u043c \u0432 <code>Reducer<\/code> \u043d\u0443\u0436\u043d\u044b\u0439 \u043a\u0435\u0439\u0441.<\/p>\n<p>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0432\u043d\u0443\u0442\u0440\u0438 <code>ViewModel<\/code>:<\/p>\n<pre><code class=\"swift\">enum Action {         case viewDidLoad         case dismiss         case updatePosition(CGFloat)         case setPriceCellVisible(Bool)         case saveLastSlidingStep(Int)         case setBottomSafeAreaInset(CGFloat)         case onNavigationHeaderAction(NavigationHeader.Action)         case onSideButtonsAction(SideButtons.Action)         case onImageSliderAction(ImageSliderAssembly.Action)         case onFooterButtonsAction(FooterButtons.Action)         case onPriceCellAction(PriceCell.Action)     }<\/code><\/pre>\n<p>\u041a\u0430\u043a \u044d\u0442\u043e <code>ViewModel<\/code> \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u0432 \u043a\u043e\u0434\u0435:<\/p>\n<pre><code class=\"swift\"> func handle(_ action: Action) async {         switch action {         case .viewDidLoad:             await viewDidLoad()         case .dismiss:             await dismiss()         case let .updatePosition(transition):             await updatePosition(for: transition)         case let .setPriceCellVisible(isPriceCellVisible):             await setPriceCellVisible(isPriceCellVisible)         case let .saveLastSlidingStep(step):             await saveSlidingStep(step)         case let .setBottomSafeAreaInset(inset):             await setBottomSafeAreaInset(inset)         case let .onNavigationHeaderAction(action):             await handleNavigationHeader(action: action)         case let .onSideButtonsAction(action):             await handleSideButtons(action: action)         case let .onImageSliderAction(action):             await handleImageSlider(action: action)         case let .onFooterButtonsAction(action):             await handleFooterButtons(action: action)         case let .onPriceCellAction(action):             await handlePriceCell(action: action)         }     }<\/code><\/pre>\n<p>\u0412\u043d\u0443\u0442\u0440\u0438 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430 <code>handle(...)<\/code> \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0439 \u043c\u0435\u0442\u043e\u0434 <code>handleImageSlider( action: ImageSliderAssembly.Action)<\/code>, \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u044e\u0449\u0438\u0439 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0438\u0439 <code>Action<\/code> \u0438\u0437 <code>Subview<\/code>. \u0417\u0434\u0435\u0441\u044c \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0430\u043d\u0430\u043b\u043e\u0433\u0438\u0447\u043d\u043e:<\/p>\n<pre><code class=\"swift\"> private func handleImageSlider(action: ImageSliderAssembly.Action) async {         switch action {         case let .onTapSlider(index):             print(index)         case let .onTapReview(index):             print(index)         case let .onSaveSlidingStep(step):             await saveSlidingStep(step)         }     }<\/code><\/pre>\n<p>\u0413\u0440\u0430\u043d\u0438\u0447\u043d\u044b\u0439 \u0441\u043b\u0443\u0447\u0430\u0439: \u043f\u0440\u043e \u0431\u043e\u043b\u044c\u0448\u043e\u0439 <code>View<\/code> \u0438 \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0443 \u0432 \u043d\u0435\u0433\u043e <code>State<\/code> \u0438 <code>Reducer<\/code>.\u00a0<\/p>\n<h2>List \u0441 \u043c\u043d\u043e\u0436\u0435\u0441\u0442\u0432\u043e\u043c \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u0445 \u044f\u0447\u0435\u0435\u043a<\/h2>\n<p>\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0439 \u043f\u0440\u0438\u043c\u0435\u0440 \u043e\u043f\u0442\u0438\u043c\u0438\u0437\u0430\u0446\u0438\u0438 \u043a\u043e\u0434\u0430 \u2014 \u044d\u0442\u043e \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u044f, \u043a\u043e\u0433\u0434\u0430 \u0438\u043c\u0435\u0435\u0442\u0441\u044f \u043d\u0435\u043a\u0438\u0439 <code>Subview<\/code> \u0441 \u0431\u043e\u043b\u044c\u0448\u0438\u043c \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e\u043c \u0441\u0432\u043e\u0438\u0445 <code>Subview<\/code>. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, <code>List<\/code>, \u0443 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u043c\u043d\u043e\u0436\u0435\u0441\u0442\u0432\u043e \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u0445 \u044f\u0447\u0435\u0435\u043a, \u043d\u0435\u0443\u0434\u043e\u0431\u043d\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0442\u044c \u0432 init \u043a\u0443\u0447\u0443 \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432, \u0447\u0442\u043e\u0431\u044b \u043e\u043d \u043f\u0435\u0440\u0435\u0434\u0430\u043b \u0438\u0445 \u0432 \u044f\u0447\u0435\u0439\u043a\u0438. \u041f\u043e \u0431\u043e\u043b\u044c\u0448\u0435\u0439 \u0447\u0430\u0441\u0442\u0438 \u0435\u0439 \u043d\u0443\u0436\u043d\u044b \u043f\u0440\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0432\u0441\u0435 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435 \u0438\u0437 <code>State<\/code> \u0438 \u0431\u041e\u043b\u044c\u0448\u0430\u044f \u0447\u0430\u0441\u0442\u044c \u043a\u0435\u0439\u0441\u043e\u0432 \u0438\u0437 <code>Action<\/code>. \u041a\u0430\u043a \u0436\u0435 \u0431\u044b\u0442\u044c \u0432 \u044d\u0442\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435? \u0410 \u043f\u043e\u0447\u0435\u043c\u0443 \u0431\u044b \u043d\u0435 \u043e\u0442\u0434\u0430\u0442\u044c \u0432 <code>List<\/code> \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e \u0432\u0435\u0441\u044c <code>State<\/code> \u0438 <code>Reducer<\/code>, \u0430 \u0443\u0436\u0435 \u0432\u043d\u0443\u0442\u0440\u0438 <code>List<\/code> \u0432\u044b\u0447\u043b\u0435\u043d\u0438\u0442\u044c \u043d\u0443\u0436\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0439 \u044f\u0447\u0435\u0439\u043a\u0438?\u00a0<\/p>\n<p>\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e \u043b\u0430\u043a\u043e\u043d\u0438\u0447\u043d\u044b\u0439 \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0442\u043e\u0440 \u0432\u043d\u0443\u0442\u0440\u0438 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 <code>View<\/code>:<\/p>\n<pre><code class=\"swift\">private var content: some View {         ProductCardList(state: state, reducer: reducer)             .offset(y: state.offset)     }<\/code><\/pre>\n<p>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0432\u043d\u0443\u0442\u0440\u0438 <code>Subview<\/code>:<\/p>\n<pre><code class=\"swift\">struct ProductCardList&lt;S, R&gt;: View where S: ProductCard.ViewState, R: Reducer&lt;ProductCard.ViewModel&gt; {     @ObservedObject var state: S     let reducer: R<\/code><\/pre>\n<p>\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0442\u043e\u0440 \u0437\u0434\u0435\u0441\u044c \u043d\u0435 \u043f\u0440\u043e\u043f\u0438\u0441\u0430\u043d \u2014 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u043e\u0439 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438.\u00a0<\/p>\n<p>\u041d\u0443 \u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u044f \u044f\u0447\u0435\u0435\u043a \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u0438\u043d\u0434\u0435\u043a\u0441\u0430, \u0430\u043d\u0430\u043b\u043e\u0433\u0438\u0447\u043d\u043e <code>cellForItem<\/code> \u0432 UIKit:<\/p>\n<pre><code class=\"swift\">@ViewBuilder     private func getCell(for index: Int) -&gt; some View {         if index == 0 {             PriceCell(                 state: state.priceCell,                 onAction: { reducer(.onPriceCellAction($0)) }             )         } else if index % 2 == 0 {             ProductCardCell(index: index)         } else {             CellDivider()         }     }<\/code><\/pre>\n<p>\u0412 \u044d\u0442\u043e\u043c \u043f\u0440\u0438\u043c\u0435\u0440\u0435 <code>ProductCardCell<\/code>, \u043f\u043e \u0441\u0443\u0442\u0438, \u043f\u0440\u043e\u0441\u0442\u043e \u043e\u0431\u0440\u0430\u0437\u0435\u0446 \u044f\u0447\u0435\u0439\u043a\u0438, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c, \u0447\u0442\u043e \u0438\u0445 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043c\u043d\u043e\u0433\u043e, \u043d\u043e \u043d\u0435 \u0443\u0441\u043b\u043e\u0436\u043d\u044f\u0442\u044c \u0441\u0430\u043c\u043e \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435.\u00a0<\/p>\n<h2>\u041f\u0440\u0438\u043c\u0435\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0441 UIKit<\/h2>\n<p>\u041d\u0430\u0448\u0430 \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0441\u043e\u0432\u043c\u0435\u0441\u0442\u043d\u043e \u0441\u043e SwiftUI \u0438 UIKit. \u0420\u0430\u0437\u043d\u0438\u0446\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0432 \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c\u0435 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439 <code>State<\/code>. \u0418 \u0442\u0430\u043c, \u0438 \u0442\u0430\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0440\u0435\u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0439 \u043f\u043e\u0434\u0445\u043e\u0434.\u00a0<\/p>\n<p>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0434\u043b\u044f SwiftUI \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0430 \u0438 \u043f\u043e\u043d\u044f\u0442\u043d\u0430, \u043d\u0438\u0436\u0435 \u043f\u0440\u0438\u0432\u043e\u0436\u0443 \u043f\u0440\u0438\u043c\u0435\u0440 \u0434\u043b\u044f UIKit, \u0433\u0434\u0435 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0441\u0442\u0430\u0442\u0443\u0441 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0434\u0430\u043d\u043d\u044b\u0445 \u0434\u043b\u044f \u044d\u043a\u0440\u0430\u043d\u0430 \u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442\u0441\u044f UI:<\/p>\n<pre><code class=\"swift\"> private func bindState() {         state             .$inputState             .receive(on: RunLoop.main)             .sink { [weak self] inputState in                 switch inputState {                 case .loading:                     self?.showSkeletonLoader(self?.state.currentType)                     self?.hideErrorView()                 case let .reloadButtonTitle(buttonInfo):                     self?.setBottomButtonTitle(response: buttonInfo)                 case .reloadDataSource:                     self?.tableManager.reloadTable()                 case .error:                     self?.showErrorView()                 }             }             .store(in: &amp;bag)     }<\/code><\/pre>\n<h2>\u0417\u0430\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435<\/h2>\n<p>\u042f \u043e\u043f\u0438\u0441\u0430\u043b, \u043a\u0430\u043a\u00a0\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u043f\u043e\u0434\u00a0\u0441\u0432\u043e\u0438 \u043d\u0443\u0436\u0434\u044b \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0443\u044e \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0443 MVVM \u0438 \u043e\u043f\u0442\u0438\u043c\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0432. \u041d\u0430\u0434\u0435\u044e\u0441\u044c, \u043c\u043e\u0439 \u043e\u043f\u0443\u0441\u00a0\u0431\u044b\u043b \u043f\u043e\u043d\u044f\u0442\u0435\u043d \u0438 \u043f\u043e\u043b\u0435\u0437\u0435\u043d \ud83d\ude42 \u0421\u043f\u0430\u0441\u0438\u0431\u043e \u0437\u0430\u00a0\u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0438 \u0443\u0434\u0430\u0447\u0438 \u0432\u00a0\u0440\u0430\u0431\u043e\u0442\u0435!<\/p>\n<p>\u0414\u043b\u044f \u0431\u043e\u043b\u0435\u0435 \u0433\u043b\u0443\u0431\u043e\u043a\u043e \u043f\u043e\u0433\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u0432 \u043f\u0440\u043e\u0435\u043a\u0442 \u043e\u0441\u0442\u0430\u0432\u043b\u044f\u044e \u0441\u0441\u044b\u043b\u043a\u0443 \u043d\u0430 <a href=\"https:\/\/github.com\/sunlight-team\/iosNewArchitecture\" rel=\"noopener noreferrer nofollow\">\u0433\u0438\u0442\u0445\u0430\u0431<\/a>.<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/913610\/\"> https:\/\/habr.com\/ru\/articles\/913610\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<figure class=\"full-width\">\n<div><figcaption>\u041a\u0430\u043a\u0430\u044f \u043b\u0443\u0447\u0448\u0435???<\/figcaption><\/div>\n<\/figure>\n<p>\u0412\u0441\u0435\u043c \u043f\u0440\u0438\u0432\u0435\u0442, \u043c\u0435\u043d\u044f \u0437\u043e\u0432\u0443\u0442 \u0414\u043c\u0438\u0442\u0440\u0438\u0439 \u041b\u043e\u0440\u0435\u043d\u0446, \u044f iOS-\u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u0432 IT-\u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 GRI. \u041d\u0430\u0448 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u043a\u043b\u0438\u0435\u043d\u0442 \u2014 Sunlight, \u0434\u043b\u044f \u043d\u0435\u0433\u043e \u043c\u044b \u0440\u0430\u0437\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u043c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u044b\u0445 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439 \u043f\u043e \u043f\u043e\u043b\u043d\u043e\u043c\u0443 \u0446\u0438\u043a\u043b\u0443 \u0438 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c \u0441\u0430\u0439\u0442.<\/p>\n<p>\u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0443 \u043f\u0440\u043e \u043d\u0430\u0448\u0443 \u043d\u043e\u0432\u0443\u044e \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0443 \u0434\u043b\u044f iOS-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438 \u043f\u043e\u0434\u0435\u043b\u044e\u0441\u044c \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u043c\u0438 \u0441\u043e\u0432\u0435\u0442\u0430\u043c\u0438, \u043a\u0430\u043a \u0443\u043f\u0440\u043e\u0441\u0442\u0438\u0442\u044c \u0441\u0435\u0431\u0435 \u0436\u0438\u0437\u043d\u044c \u0438 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u043a\u043e\u0434 \u0431\u043e\u043b\u0435\u0435 \u043b\u0430\u043a\u043e\u043d\u0438\u0447\u043d\u044b\u043c \u0438 \u0447\u0438\u0442\u0430\u0435\u043c\u044b\u043c.<\/p>\n<p>\u0417\u0430 \u043e\u0441\u043d\u043e\u0432\u0443 \u043c\u044b \u0432\u0437\u044f\u043b\u0438 \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0443\u00a0MVVM (<code>Model<\/code>\u2014<code>View<\/code>\u2014<code>ViewModel<\/code>), \u0432\u00a0\u043a\u043e\u0442\u043e\u0440\u043e\u0439 <code>View<\/code> \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430\u00a0\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445, \u0432\u0441\u044f \u0431\u0438\u0437\u043d\u0435\u0441 \u043b\u043e\u0433\u0438\u043a\u0430 \u0441\u043e\u0441\u0440\u0435\u0434\u043e\u0442\u043e\u0447\u0435\u043d\u0430 \u0432\u043d\u0443\u0442\u0440\u0438 <code>ViewModel<\/code>. <code>ViewModel<\/code> \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043e\u0442 <code>View<\/code>, \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442 \u0441\u0432\u043e\u0438 \u0434\u0430\u043d\u043d\u044b\u0435, \u0438 <code>View<\/code> \u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043e\u043c data binding \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442 \u0441\u0432\u043e\u0451 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435, \u0447\u0442\u043e \u043e\u0447\u0435\u043d\u044c \u0443\u0434\u043e\u0431\u043d\u043e. <code>Model<\/code>\u00a0\u2014 \u043c\u043e\u0434\u0435\u043b\u044c \u0434\u043b\u044f\u00a0\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0438 \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0434\u0430\u043d\u043d\u044b\u0445. <\/p>\n<p>\u0422\u0430\u043a\u0436\u0435 \u043c\u044b \u043e\u0431\u0440\u0430\u0442\u0438\u043b\u0438 \u0441\u0432\u043e\u0439 \u0432\u0437\u043e\u0440 \u0432 \u0441\u0442\u043e\u0440\u043e\u043d\u0443 TCA, \u0443 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0435\u0441\u0442\u044c:<\/p>\n<ul>\n<li>\n<p><code>UI<\/code>\u00a0\u2014 \u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445;<\/p>\n<\/li>\n<li>\n<p><code>Action<\/code>\u00a0\u2014 \u043d\u0430\u0431\u043e\u0440 \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0445 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439;<\/p>\n<\/li>\n<li>\n<p><code>State<\/code>\u00a0\u2014 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445;<\/p>\n<\/li>\n<li>\n<p><code>Environment<\/code>\u00a0\u2014 \u043d\u0430\u0431\u043e\u0440 \u0432\u043d\u0435\u0448\u043d\u0438\u0445 \u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432;<\/p>\n<\/li>\n<li>\n<p><code>Reducer<\/code>\u00a0\u2014 \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c, \u043c\u0435\u043d\u044f\u044e\u0449\u0438\u0439 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0438 \u043f\u043e\u0440\u043e\u0436\u0434\u0430\u044e\u0449\u0438\u0439 \u044d\u0444\u0444\u0435\u043a\u0442\u044b;<\/p>\n<\/li>\n<li>\n<p><code>Effect<\/code>\u00a0\u2014 \u0437\u0430\u0434\u0430\u0447\u0430, \u043f\u043e\u00a0\u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0432 Reducer \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442\u0441\u044f <code>Action<\/code>.<\/p>\n<\/li>\n<\/ul>\n<p>\u041a\u0430\u043a\u0438\u0435 \u0431\u044b\u043b\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u0438 \u043f\u043e\u0447\u0435\u043c\u0443 \u0440\u0435\u0448\u0438\u043b\u0438 \u0434\u043e\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0442\u044c \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0443? \u041f\u0440\u043e\u0435\u043a\u0442 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f Sunlight \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0441\u0442\u0430\u0440\u044b\u0439, \u0434\u0430 \u0438 \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044f \u043d\u0430 SwiftUI \u043d\u0435 \u0442\u0430\u043a \u0434\u0430\u0432\u043d\u043e \u043e\u0431\u0440\u0435\u043b\u0430 \u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u043e\u0441\u0442\u044c, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0432\u0441\u044f \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044f \u0432 \u043f\u0440\u043e\u0435\u043a\u0442\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0447\u0435\u0440\u0435\u0437 UIKit. SwiftUI, \u043d\u0430\u0447\u0438\u043d\u0430\u044f \u0441 iOS 14, \u0431\u043e\u043b\u0435\u0435-\u043c\u0435\u043d\u0435\u0435 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0432\u0435\u0440\u0441\u0442\u0430\u0442\u044c \u0441\u043b\u043e\u0436\u043d\u044b\u0435 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u044b, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0431\u044b\u043b\u0430 \u0437\u0430\u0434\u0430\u0447\u0430 \u0434\u0435\u043b\u0430\u0442\u044c \u043c\u043e\u0434\u0443\u043b\u0438 \u043a\u0430\u043a \u043d\u0430 UIKit, \u0442\u0430\u043a \u0438 \u043d\u0430 SwiftUI. \u0421\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e, \u043d\u0443\u0436\u0435\u043d \u0431\u044b\u043b Builder \u043c\u043e\u0434\u0443\u043b\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u043a\u0430\u043a <code>UIVIew<\/code>, \u0442\u0430\u043a \u0438 <code>View<\/code>, \u0438 \u043e\u0442\u0434\u0430\u0451\u0442 <code>UIViewController<\/code>. \u041f\u0440\u0438 \u044d\u0442\u043e\u043c \u0431\u044b\u043b\u043e  \u0431\u043e\u043b\u044c\u0448\u043e\u0435 \u0436\u0435\u043b\u0430\u043d\u0438\u0435 \u0430\u0431\u0441\u0442\u0440\u0430\u0433\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043e\u0442 \u043c\u043d\u043e\u0433\u043e\u043f\u043e\u0442\u043e\u0447\u043d\u043e\u0441\u0442\u0438, \u0443\u043d\u0435\u0441\u0442\u0438 \u0435\u0451 \u0432 \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0443 \u043c\u043e\u0434\u0443\u043b\u044f \u0438 \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0443 \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u0451\u0440\u0441\u0442\u043a\u0443 \u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0438, \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0438 \u043e \u0447\u0451\u043c \u043d\u0435 \u0434\u0443\u043c\u0430\u044f. <\/p>\n<p>\u041a\u0430\u043a \u0438\u0442\u043e\u0433 \u043e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f MVVM \u0438 TCA \u0440\u043e\u0434\u0438\u043b\u0430\u0441\u044c \u043a\u043e\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f, \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0449\u0430\u044f \u0432 \u0441\u0435\u0431\u0435 \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u0438\u0437 MVVM: <code>Model<\/code>, <code>View<\/code>, <code>ViewModel<\/code>, \u0438 \u043f\u043e\u043c\u0438\u043c\u043e \u044d\u0442\u043e\u0433\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u043b\u0438\u0441\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0438\u0437 TCA: <code>State<\/code>, <code>Reducer<\/code> \u0438 <code>Action<\/code>. \u0420\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u0438\u0445 \u043f\u043e \u043f\u043e\u0440\u044f\u0434\u043a\u0443.<\/p>\n<h2>State<\/h2>\n<p><strong>State<\/strong> \u2014 \u044d\u0442\u043e \u043e\u0431\u0449\u0430\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u0441 \u0434\u0430\u043d\u043d\u044b\u043c\u0438, \u0445\u0440\u0430\u043d\u044f\u0449\u0430\u044f \u0432 \u0441\u0435\u0431\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 <code>View<\/code> \u0438 \u0435\u0433\u043e <code>Subview<\/code>. \u041e\u0431\u044b\u0447\u043d\u043e \u0432 MVVM \u0432\u0441\u0435 \u044d\u0442\u0438 \u0434\u0430\u043d\u043d\u044b\u0435 \u00ab\u0440\u043e\u0441\u0441\u044b\u043f\u044c\u044e\u00bb \u0445\u0440\u0430\u043d\u044f\u0442\u0441\u044f \u0432\u043e <code>ViewModel<\/code>, \u0430 \u0432 \u043d\u0430\u0448\u0435\u0439 \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0435 \u0432\u0441\u0435 UI-\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0432\u044b\u043d\u0435\u0441\u0435\u043d\u044b \u0432 <code>State<\/code>. \u041e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u0434\u043b\u044f \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f <code>ViewModel<\/code>, \u043b\u0435\u0436\u0430\u0442 \u0432\u043d\u0443\u0442\u0440\u0438 <code>ViewModel<\/code>. \u041d\u0438\u0436\u0435 \u2014 \u043f\u0440\u0438\u043c\u0435\u0440 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 <code>State<\/code> \u0441 <code>@Published<\/code> \u0438 \u0432\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u043c\u044b\u043c\u0438 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u043c\u0438.<\/p>\n<details class=\"spoiler\">\n<summary>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f State<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"swift\">import Combine import UIKit  final class ProductCardState: ViewStateProtocol { \u00a0\u00a0\u00a0\u00a0\/\/ MARK: \u2014 Properties  \u00a0\u00a0\u00a0\u00a0@Published var article: String \u00a0\u00a0\u00a0\u00a0@Published var loadingState: LoadingState \u00a0\u00a0\u00a0\u00a0@Published var position: Position \u00a0\u00a0\u00a0\u00a0@Published var currentSlidingStep: Int \u00a0\u00a0\u00a0\u00a0@Published var isImageSliderVertical: Bool \u00a0\u00a0\u00a0\u00a0@Published var productImages: [NetworkImage] \u00a0\u00a0\u00a0\u00a0@Published var actualPrice: String \u00a0\u00a0\u00a0\u00a0@Published var initialPrice: String \u00a0\u00a0\u00a0\u00a0@Published var basketLoadingState: LoadingState \u00a0\u00a0\u00a0\u00a0@Published var isPriceCellVisible: Bool \u00a0\u00a0\u00a0\u00a0@Published var isAvailableToBuy: Bool \u00a0\u00a0\u00a0\u00a0@Published var isAddedToBasket: Bool \u00a0\u00a0\u00a0\u00a0@Published var bottomSafeAreaInset: CGFloat \u00a0\u00a0\u00a0\u00a0@Published var priceDescription: String  \u00a0\u00a0\u00a0\u00a0var shouldShowPriceInButton: Bool { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if case .bottom = position { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return true \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0} \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return !isPriceCellVisible \u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0var navigationHeaderOpacity: Double { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0switch position { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0case .bottom: \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a00 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0case .middle: \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a00 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0case .top: \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a01 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0} \u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0var offset: Double { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0switch position { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0case .bottom: \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0UIScreen.main.bounds.height \u2014 PublicConstant.initialOffset \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0case .middle: \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0isImageSliderVertical ? UIScreen.main.bounds.height \/ 2.0 : UIScreen.main.bounds.width \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0case .top: \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0PublicConstant.navBarHeight \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0} \u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0var navigationHeader: NavigationHeader.ViewState { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0.init( \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0article: article, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0loadingState: loadingState, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0opacity: navigationHeaderOpacity \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0) \u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0var priceCell: PriceCell.ViewState { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0.init( \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0name: \"\u0421\u0435\u0440\u0435\u0431\u0440\u044f\u043d\u043d\u044b\u0435 \u0447\u0430\u0441\u044b Bastet. \u0428\u0432\u0435\u0439\u0446\u0430\u0440\u0441\u043a\u0438\u0439 \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c \u0438 \u0437\u043d\u0430\u043c\u0435\u043d\u0438\u0442\u044b\u0435 \u0411\u0435\u043b\u043e\u0440\u0443\u0441\u0441\u043a\u0438\u0435 \u0441\u0442\u0440\u0435\u043b\u043a\u0438\", \/\/ stub data \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0bages: [\"\u041d\u041e\u0412\u0418\u041d\u041a\u0410\", \"\u0425\u0418\u0422\", \"\u0418\u0422\u0410\u041b\u0418\u042f\"], \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0actualPrice: actualPrice, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0initialPrice: initialPrice, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0priceDescription: priceDescription, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0position: position \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0) \u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0var footerButtons: FooterButtons.ViewState { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0.init( \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0loadingState: loadingState, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0bottomInset: bottomSafeAreaInset, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0basketLoadingState: basketLoadingState, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0actualPrice: actualPrice, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0initialPrice: initialPrice, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0shouldShowPriceInButton: shouldShowPriceInButton, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0isAvailableToBuy: isAvailableToBuy, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0isAddedToBasket: isAddedToBasket \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0) \u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0var imageSliderAssembly: ImageSliderAssembly.ViewState { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0get { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0.init( \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0currentStep: currentSlidingStep, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0loadingState: loadingState, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0position: position, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0initialOffset: PublicConstant.initialOffset, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0isImageSliderVertical: isImageSliderVertical, \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0productImages: productImages \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0) \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0set { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0currentSlidingStep = newValue.currentStep \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0} \u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0\/\/ MARK: \u2014 Lifecycle \u00a0\u00a0\u00a0\u00a0init(input: ProductCard.Input?) { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0article = input?.article ?? \"\" \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0loadingState = .loading \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0position = .bottom \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0currentSlidingStep = 0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0isImageSliderVertical = true \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0productImages = [] \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0actualPrice = \"\" \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0initialPrice = \"\" \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0basketLoadingState = .hide \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0isPriceCellVisible = false \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0isAvailableToBuy = true \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0isAddedToBasket = false \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0bottomSafeAreaInset = 0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0priceDescription = \"\" \u00a0\u00a0\u00a0\u00a0} }  extension ProductCard.ViewState { \u00a0\u00a0\u00a0\u00a0enum Position { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0case bottom \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0case middle \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0case top \u00a0\u00a0\u00a0\u00a0} }  extension ProductCard.ViewState { \u00a0\u00a0\u00a0\u00a0enum PublicConstant { \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0static let initialOffset = 146.0 \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0static let navBarHeight = 104.0   } }<\/code><\/pre>\n<\/div>\n<\/details>\n<p><code>State<\/code> \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 <code>ViewStateProtocol<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0438\u043c\u0435\u0435\u0442 \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0442\u043e\u0440 \u0441 <code>Input<\/code>: \u0447\u0442\u043e\u0431\u044b \u0431\u044b\u043b\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0442\u044c \u0432\u0445\u043e\u0434\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u043c\u043e\u0434\u0443\u043b\u044c \u0438 \u043c\u0435\u0442\u043e\u0434 <code>update()<\/code> \u0434\u043b\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432 \u043f\u043e\u0442\u043e\u043a\u0435 <code>main<\/code>.<\/p>\n<details class=\"spoiler\">\n<summary>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f ViewStateProtocol<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"swift\">\/\/ MARK: \u2014 ViewState  @MainActor protocol ViewStateProtocol: ObservableObject, Sendable { \u00a0\u00a0\u00a0\u00a0associatedtype Input \u00a0\u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0\u00a0init(input: Input?) }  \u00a0\u00a0\u00a0\u00a0  extension ViewStateProtocol {  func update(_ handler: @Sendable @MainActor (Self) -&gt; Void) async { \u00a0\u00a0\u00a0\u00a0await MainActor.run { handler(self) } \u00a0\u00a0\u00a0\u00a0} }<\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u042d\u0442\u043e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e, \u0442\u0430\u043a \u043a\u0430\u043a \u0432\u0441\u0435 UI-\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b, \u043e\u0442\u0432\u0435\u0447\u0430\u044e\u0449\u0438\u0435 \u0437\u0430 \u0432\u043d\u0435\u0448\u043d\u0438\u0439 \u0432\u0438\u0434 <code>View<\/code> \u0438 \u0435\u0433\u043e <code>Subview<\/code>, \u0445\u0440\u0430\u043d\u044f\u0442\u0441\u044f \u0432\u043d\u0443\u0442\u0440\u0438 <code>State<\/code>. \u0421\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e, \u043f\u0440\u0438 \u0438\u0445 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 <code>View<\/code> \u0441\u0440\u0430\u0437\u0443 \u043f\u0435\u0440\u0435\u0440\u0438\u0441\u043e\u0432\u044b\u0432\u0430\u0435\u0442 \u0441\u0432\u043e\u0451 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435.\u00a0 \u0421\u0430\u043c \u043f\u043e \u0441\u0435\u0431\u0435 <code>State<\/code> \u2014 \u044d\u0442\u043e \u043a\u043b\u0430\u0441\u0441, \u0442\u0430\u043a \u0443\u0434\u043e\u0431\u043d\u0435\u0435 \u0435\u0433\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0432 \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u0445 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044f\u0445 (<code>View<\/code>, <code>ViewModel<\/code>), \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0442\u044c \u0432 <code>Subview<\/code> (\u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u0434\u0430\u043b\u0435\u0435), \u0438 \u0432\u0441\u0435\u0433\u0434\u0430 \u044d\u0442\u043e \u043e\u0434\u0438\u043d \u0438 \u0442\u043e\u0442 \u0436\u0435 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440.\u00a0<\/p>\n<h2>View<\/h2>\n<p>View \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 <code>ViewProtocol<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0432 \u0441\u0432\u043e\u044e \u043e\u0447\u0435\u0440\u0435\u0434\u044c \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u0430\u0433\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0443 \u0432 \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0442\u043e\u0440 <code>State<\/code> \u0438 <code>Reducer<\/code> (\u043e \u043d\u0451\u043c \u0447\u0443\u0442\u044c \u043f\u043e\u0437\u0436\u0435).<\/p>\n<pre><code class=\"swift\">\/\/ MARK: \u2014 View protocol ViewProtocol {     associatedtype ViewState: ViewStateProtocol     associatedtype ViewModel: ViewModelProtocol          @MainActor     init(state: ViewState, reducer: Reducer&lt;ViewModel&gt;) }<\/code><\/pre>\n<details class=\"spoiler\">\n<summary>\u041f\u0440\u0438\u043c\u0435\u0440 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 View<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"swift\">import SwiftUI struct ProductCardView: View, ViewProtocol {        @ObservedObject var state: ProductCard.ViewState     let reducer: Reducer&lt;ProductCard.ViewModel&gt;        private var isDragGestureEnabled: Bool {         if case .bottom = state.position {             return true         }         return false     }        init(state: ProductCard.ViewState, reducer: Reducer&lt;ProductCard.ViewModel&gt;) {         self.state = state         self.reducer = reducer     }        var body: some View {         ProductCardViewLayout(             header: { header },             sideButtons: { sideButtons },             slider: { slider },             content: { content },             footer: { footer(geometry: $0) }         )         .onAppear { reducer(.viewDidLoad) }         .animation(.easeInOut(duration: 1.0), value: state.position)         .animation(.default, value: state.isPriceCellVisible)         .animation(.default, value: state.isAddedToBasket)         .animation(.default, value: state.basketLoadingState)         .animation(.default, value: state.loadingState)     }        private var header: some View {         NavigationHeader(             state: state.navigationHeader,             onAction: { reducer(.onNavigationHeaderAction($0)) }         )     }        private var sideButtons: some View {         SideButtons(             position: state.position,             loadingState: state.loadingState,             onAction: { reducer(.onSideButtonsAction($0)) }         )         .opacity(1 \u2014 state.navigationHeaderOpacity)     }        private var slider: some View {         ImageSliderAssembly(             state: $state.imageSliderAssembly,             onAction: { reducer(.onImageSliderAction($0)) }         )     }        private var content: some View {         ProductCardList(state: state, reducer: reducer)             .offset(y: state.offset)     }        private func footer(geometry: GeometryProxy) -&gt; some View {         FooterButtons(             state: state.footerButtons,             onAction: { reducer(.onFooterButtonsAction($0)) }         )         .onAppear {             reducer(.setBottomSafeAreaInset(geometry.safeAreaInsets.bottom))         }         .animation(.easeInOut(duration: 1.0), value: state.position)         .animation(.default, value: state.isPriceCellVisible)         .animation(.default, value: state.isAddedToBasket)         .animation(.default, value: state.basketLoadingState)         .animation(.default, value: state.loadingState)     } }<\/code><\/pre>\n<\/div>\n<\/details>\n<p><code>View<\/code> \u043c\u043e\u0436\u0435\u0442 \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e \u0447\u0438\u0442\u0430\u0442\u044c \u0432\u0441\u0435 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430 <code>State<\/code> \u0438 \u0434\u0430\u0436\u0435 \u0438\u0437\u043c\u0435\u043d\u044f\u0442\u044c \u0438\u0445 \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 <code>Binding<\/code>. <\/p>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-463200","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/463200","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=463200"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/463200\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=463200"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=463200"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=463200"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}