Structuring Apps
When you create larger iced apps, you might want to have reusable components and views. For that, there is a common approach that is also used in this showcase from the founder of iced.
In this architecture a view/component is independent of the application and is split into three parts, message, the state and the action.
The state will have a normal view function that returns a iced::Element<Message>
.
The update function will differ a bit. Instead of a Task like our main application, it will return an action enum.
Common variants for this enum can be None
, Task(iced::Task<Message>)
and Submit(String)
. You can think of this action like a message that is sent from the view to the parent application that should handle it.
This kind of architecture will enhance composability.
Initial State
The view should also have a function that creates a new instance of that view.
If some task needs to be done for the initial state of the view (i.e. fetching data), something like (Self, iced::Task<Message>)
should be returned by that function.
The task would be executed, and the data is sent via a message to the view.
To differ between a loaded and not loaded state you can use something like this pattern for your state:
enum State {
NotLoaded,
Loaded {
field1: String,
field2: i32
}
}
If you do not need to lazy load something from an API or do similar, you can just have a normal struct as state.
Mapping
If you want to compose your UI of subparts that have their own functionality, it makes sense to give them their own messages.
But to use a iced::Element<ViewMessage>
in your app that requires a iced::Element<AppMessage>
you have to map them.
For that, you can simply use iced::Element<ViewMessage>.map(AppMessage::ViewMessage)
.
Note: this is a shortcut for
iced::Element<ViewMessage>.map(|view_message| AppMessage::ViewMessage(view_message))
The .map
function takes a closure that takes the message and converts it into another message.
This often requires having a dedicated message variant on the application level that contains the view message.
Map functions like this are available on Task::map
, Subscription::map
, Element::map
.
Example
The example shows an unfinished joke listing app with a view for creating jokes. When the user tries to create a new joke, a default joke, fetched from an API, is provided. The user can get a random joke from the API on a button click as well. All Jokes are listed in the main/default view.
This is how the view for new jokes looks like using this design:
fn main() {
iced::run("Project Structure Example", App::update, App::view).unwrap();
}
#[derive(Debug, Clone)]
enum Message {
// This message is used to handle the new views message
NewMessage(new::Message),
New,
}
#[derive(Debug, Default)]
enum View {
#[default]
Default,
Edit(new::NewView),
}
#[derive(Debug, Default)]
struct App {
view: View,
items: Vec<String>,
}
impl App {
fn update(&mut self, message: Message) -> iced::Task<Message> {
match message {
Message::NewMessage(view_message) => {
if let View::Edit(edit) = &mut self.view {
// Call the update method of the edit view
// and handle the returned action
match edit.update(view_message) {
new::Action::None => {}
// If the action is a task, map the
// task to a message, the higher level message
new::Action::Task(task) => return task.map(Message::NewMessage),
new::Action::Submitted(content) => {
self.view = View::Default;
self.items.push(content);
}
}
}
}
Message::New => {
// Create a new view
let (view, task) = new::NewView::new();
self.view = View::Edit(view);
// Run the task and map it to the higher level message
return task.map(Message::NewMessage);
}
}
iced::Task::none()
}
fn view(&self) -> iced::Element<Message> {
match &self.view {
View::Default => {
let items = self
.items
.iter()
.map(|item| iced::widget::text(item).into())
.collect();
iced::widget::column![
iced::widget::button("Edit").on_press(Message::New),
iced::widget::Column::from_vec(items)
]
.into()
}
// If the view is an edit view, call the view method of the edit view
// and map the returned message to the higher level message
View::Edit(edit) => edit.view().map(Message::NewMessage),
}
}
}
mod new {
pub enum Action {
None,
Task(iced::Task<Message>),
Submitted(String),
}
#[derive(Debug, Clone)]
pub enum Message {
Submit,
ChangeContent(String),
RandomJoke,
}
#[derive(Debug, Default)]
pub struct NewView {
content: String,
}
impl NewView {
pub fn new() -> (Self, iced::Task<Message>) {
(Self::default(), Self::random_joke_task())
}
pub fn update(&mut self, message: Message) -> Action {
match message {
Message::Submit => Action::Submitted(self.content.clone()),
Message::ChangeContent(content) => {
self.content = content;
Action::None
}
Message::RandomJoke => Action::Task(Self::random_joke_task()),
}
}
pub fn view(&self) -> iced::Element<Message> {
iced::widget::column![
iced::widget::text_input("Content", &self.content).on_input(Message::ChangeContent),
iced::widget::button("Random Joke").on_press(Message::RandomJoke),
iced::widget::button("Submit").on_press(Message::Submit)
]
.into()
}
fn random_joke_task() -> iced::Task<Message> {
iced::Task::future(async {
// Fetch a joke from the internet
let client = reqwest::Client::new();
let response: serde_json::Value = client
.get("https://icanhazdadjoke.com")
.header("Accept", "application/json")
.send()
.await
.unwrap()
.json()
.await
.unwrap();
// Parse the response
let joke = response["joke"].as_str().unwrap();
// Return the joke as a message
Message::ChangeContent(joke.to_owned())
})
}
}
}
As you see, the update function produces this action, that the parent of the view can handle:
fn main() {
iced::run("Project Structure Example", App::update, App::view).unwrap();
}
#[derive(Debug, Clone)]
enum Message {
// This message is used to handle the new views message
NewMessage(new::Message),
New,
}
#[derive(Debug, Default)]
enum View {
#[default]
Default,
Edit(new::NewView),
}
#[derive(Debug, Default)]
struct App {
view: View,
items: Vec<String>,
}
impl App {
fn update(&mut self, message: Message) -> iced::Task<Message> {
match message {
Message::NewMessage(view_message) => {
if let View::Edit(edit) = &mut self.view {
// Call the update method of the edit view
// and handle the returned action
match edit.update(view_message) {
new::Action::None => {}
// If the action is a task, map the
// task to a message, the higher level message
new::Action::Task(task) => return task.map(Message::NewMessage),
new::Action::Submitted(content) => {
self.view = View::Default;
self.items.push(content);
}
}
}
}
Message::New => {
// Create a new view
let (view, task) = new::NewView::new();
self.view = View::Edit(view);
// Run the task and map it to the higher level message
return task.map(Message::NewMessage);
}
}
iced::Task::none()
}
fn view(&self) -> iced::Element<Message> {
match &self.view {
View::Default => {
let items = self
.items
.iter()
.map(|item| iced::widget::text(item).into())
.collect();
iced::widget::column![
iced::widget::button("Edit").on_press(Message::New),
iced::widget::Column::from_vec(items)
]
.into()
}
// If the view is an edit view, call the view method of the edit view
// and map the returned message to the higher level message
View::Edit(edit) => edit.view().map(Message::NewMessage),
}
}
}
mod new {
pub enum Action {
None,
Task(iced::Task<Message>),
Submitted(String),
}
#[derive(Debug, Clone)]
pub enum Message {
Submit,
ChangeContent(String),
RandomJoke,
}
#[derive(Debug, Default)]
pub struct NewView {
content: String,
}
impl NewView {
pub fn new() -> (Self, iced::Task<Message>) {
(Self::default(), Self::random_joke_task())
}
pub fn update(&mut self, message: Message) -> Action {
match message {
Message::Submit => Action::Submitted(self.content.clone()),
Message::ChangeContent(content) => {
self.content = content;
Action::None
}
Message::RandomJoke => Action::Task(Self::random_joke_task()),
}
}
pub fn view(&self) -> iced::Element<Message> {
iced::widget::column![
iced::widget::text_input("Content", &self.content).on_input(Message::ChangeContent),
iced::widget::button("Random Joke").on_press(Message::RandomJoke),
iced::widget::button("Submit").on_press(Message::Submit)
]
.into()
}
fn random_joke_task() -> iced::Task<Message> {
iced::Task::future(async {
// Fetch a joke from the internet
let client = reqwest::Client::new();
let response: serde_json::Value = client
.get("https://icanhazdadjoke.com")
.header("Accept", "application/json")
.send()
.await
.unwrap()
.json()
.await
.unwrap();
// Parse the response
let joke = response["joke"].as_str().unwrap();
// Return the joke as a message
Message::ChangeContent(joke.to_owned())
})
}
}
}
The application that would host the view could look like this:
fn main() {
iced::run("Project Structure Example", App::update, App::view).unwrap();
}
#[derive(Debug, Clone)]
enum Message {
// This message is used to handle the new views message
NewMessage(new::Message),
New,
}
#[derive(Debug, Default)]
enum View {
#[default]
Default,
Edit(new::NewView),
}
#[derive(Debug, Default)]
struct App {
view: View,
items: Vec<String>,
}
impl App {
fn update(&mut self, message: Message) -> iced::Task<Message> {
match message {
Message::NewMessage(view_message) => {
if let View::Edit(edit) = &mut self.view {
// Call the update method of the edit view
// and handle the returned action
match edit.update(view_message) {
new::Action::None => {}
// If the action is a task, map the
// task to a message, the higher level message
new::Action::Task(task) => return task.map(Message::NewMessage),
new::Action::Submitted(content) => {
self.view = View::Default;
self.items.push(content);
}
}
}
}
Message::New => {
// Create a new view
let (view, task) = new::NewView::new();
self.view = View::Edit(view);
// Run the task and map it to the higher level message
return task.map(Message::NewMessage);
}
}
iced::Task::none()
}
fn view(&self) -> iced::Element<Message> {
match &self.view {
View::Default => {
let items = self
.items
.iter()
.map(|item| iced::widget::text(item).into())
.collect();
iced::widget::column![
iced::widget::button("Edit").on_press(Message::New),
iced::widget::Column::from_vec(items)
]
.into()
}
// If the view is an edit view, call the view method of the edit view
// and map the returned message to the higher level message
View::Edit(edit) => edit.view().map(Message::NewMessage),
}
}
}
mod new {
pub enum Action {
None,
Task(iced::Task<Message>),
Submitted(String),
}
#[derive(Debug, Clone)]
pub enum Message {
Submit,
ChangeContent(String),
RandomJoke,
}
#[derive(Debug, Default)]
pub struct NewView {
content: String,
}
impl NewView {
pub fn new() -> (Self, iced::Task<Message>) {
(Self::default(), Self::random_joke_task())
}
pub fn update(&mut self, message: Message) -> Action {
match message {
Message::Submit => Action::Submitted(self.content.clone()),
Message::ChangeContent(content) => {
self.content = content;
Action::None
}
Message::RandomJoke => Action::Task(Self::random_joke_task()),
}
}
pub fn view(&self) -> iced::Element<Message> {
iced::widget::column![
iced::widget::text_input("Content", &self.content).on_input(Message::ChangeContent),
iced::widget::button("Random Joke").on_press(Message::RandomJoke),
iced::widget::button("Submit").on_press(Message::Submit)
]
.into()
}
fn random_joke_task() -> iced::Task<Message> {
iced::Task::future(async {
// Fetch a joke from the internet
let client = reqwest::Client::new();
let response: serde_json::Value = client
.get("https://icanhazdadjoke.com")
.header("Accept", "application/json")
.send()
.await
.unwrap()
.json()
.await
.unwrap();
// Parse the response
let joke = response["joke"].as_str().unwrap();
// Return the joke as a message
Message::ChangeContent(joke.to_owned())
})
}
}
}